refator(ui): merge of ResourceList part1
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Two-slot action-bar layout used above every ResourceList surface.
|
||||
*
|
||||
* [ start-slot ] ← page actions / batch actions
|
||||
* [ end-slot ] ← <DisplayModeControls /> — group-by, sort, view, dotfile
|
||||
*
|
||||
* This is pure layout — no state, no visual variation per section.
|
||||
* It reuses the existing `.actions-bar` / `.action-buttons` classes
|
||||
* defined globally in `styles/ported/content.css` so it renders
|
||||
* identically to `<ListToolbar>` (the component it's replacing).
|
||||
*
|
||||
* Consumers pass whatever they want on either side; `<ResourceList>`
|
||||
* uses this internally to wire its `actions` / `batchActions` /
|
||||
* display-mode-controls snippets, and pages can also use it directly
|
||||
* when they need a bespoke layout that doesn't fit ResourceList's
|
||||
* default (e.g. `/files` upload split-button).
|
||||
*/
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** Left cluster — page action buttons (or batch actions on
|
||||
* selection). Omit to render an empty placeholder that still
|
||||
* reserves the space, so the end cluster stays right-aligned. */
|
||||
start?: Snippet;
|
||||
/** Right cluster — usually a `<DisplayModeControls />` instance,
|
||||
* but any content works. */
|
||||
end?: Snippet;
|
||||
}
|
||||
|
||||
let { start, end }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="actions-bar">
|
||||
{#if start}{@render start()}{:else}<div class="action-buttons"></div>{/if}
|
||||
{#if end}{@render end()}{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,194 @@
|
||||
<script lang="ts" module>
|
||||
/** Reuses the existing GroupOption shape from `<ListToolbar>` so
|
||||
* callers can pass the same `groupBys` arrays their pages already
|
||||
* define. Duplicated here so consumers can import a coherent set
|
||||
* without pulling in the legacy toolbar. */
|
||||
export interface GroupOption {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Right-hand cluster of display-mode controls: group-by menu,
|
||||
* sort-direction toggle, grid/list view toggle, hide-dotfiles eye.
|
||||
*
|
||||
* Every control is opt-in via its own `show…` prop so a section
|
||||
* without one (e.g. `/trash` has no dotfile toggle by design) can
|
||||
* omit the prop rather than pass an empty array or a no-op
|
||||
* callback. State bindings pass through — the parent still owns
|
||||
* `groupBy`, `reversed`, `viewMode`, etc.
|
||||
*
|
||||
* Style-wise this reuses the ported `.view-toggle` block + child
|
||||
* classes (buttons.css) so it renders identically to the
|
||||
* `<ListToolbar>` right cluster. That keeps every page's look
|
||||
* consistent across the ResourceList migration.
|
||||
*/
|
||||
import type { Snippet } from 'svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||
import { preferences } from '$lib/stores/preferences.svelte';
|
||||
|
||||
interface Props {
|
||||
// ── Group-by ─────────────────────────────────────────────
|
||||
/** Group-by dimensions; omit or empty array = hide the control. */
|
||||
groups?: GroupOption[];
|
||||
/** Active group-by key (controlled by the parent). */
|
||||
groupBy?: string;
|
||||
/** Fired when a group-by dimension is chosen. */
|
||||
ongroup?: (key: string) => void;
|
||||
|
||||
// ── Sort direction ───────────────────────────────────────
|
||||
/** Whether the sort direction is reversed. */
|
||||
reversed?: boolean;
|
||||
/** Fired when the sort-direction toggle is clicked. */
|
||||
ondirection?: () => void;
|
||||
/** Show the sort-direction toggle. Defaults to `true` when
|
||||
* `groups` is non-empty (there's nothing to reverse otherwise). */
|
||||
showSort?: boolean;
|
||||
|
||||
// ── View mode (grid / list) ─────────────────────────────
|
||||
/** Show the grid/list view toggle. */
|
||||
showViewMode?: boolean;
|
||||
|
||||
// ── Dotfile visibility ──────────────────────────────────
|
||||
/** Show the hide-dotfiles eye toggle. Only makes sense on
|
||||
* algorithmic listings (files, recent); off by default. */
|
||||
showDotfileToggle?: boolean;
|
||||
|
||||
// ── Extension slot ──────────────────────────────────────
|
||||
/** Rendered immediately before the group-by button, still
|
||||
* inside `.view-toggle`. Kind-filter dropdowns and other
|
||||
* page-local controls that want to sit alongside the
|
||||
* built-ins land here. */
|
||||
beforeGroupBy?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
groups,
|
||||
groupBy = '',
|
||||
ongroup,
|
||||
reversed = false,
|
||||
ondirection,
|
||||
showSort,
|
||||
showViewMode = false,
|
||||
showDotfileToggle = false,
|
||||
beforeGroupBy
|
||||
}: Props = $props();
|
||||
|
||||
// Sort toggle defaults ON when a group-by list is provided —
|
||||
// there's nothing to reverse without it.
|
||||
const sortVisible = $derived(showSort ?? (groups?.length ?? 0) > 0);
|
||||
|
||||
const active = $derived(groups?.find((g) => g.key === groupBy) ?? groups?.[0]);
|
||||
let menuOpen = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!menuOpen) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
if (!(e.target as HTMLElement).closest('.group-by-selector')) menuOpen = false;
|
||||
};
|
||||
window.addEventListener('pointerdown', onDown);
|
||||
return () => window.removeEventListener('pointerdown', onDown);
|
||||
});
|
||||
|
||||
function pick(key: string) {
|
||||
menuOpen = false;
|
||||
ongroup?.(key);
|
||||
}
|
||||
|
||||
// Hide the whole cluster if nothing is enabled — the ActionBar
|
||||
// then collapses to its start-only layout without an empty
|
||||
// right block occupying space.
|
||||
const anyVisible = $derived(
|
||||
(groups?.length ?? 0) > 0 || showViewMode || showDotfileToggle || !!beforeGroupBy
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if anyVisible}
|
||||
<div class="view-toggle" role="group" aria-label={t('view.label', 'View options')}>
|
||||
{#if beforeGroupBy}{@render beforeGroupBy()}{/if}
|
||||
{#if groups?.length}
|
||||
<div class="group-by-selector" data-testid="display-mode-groupby-menu">
|
||||
<button
|
||||
class="toggle-btn group-by-btn active"
|
||||
title={t('groupby.title', 'Group by')}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={menuOpen}
|
||||
data-testid="display-mode-groupby-btn"
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
>
|
||||
<Icon name={active?.icon ?? 'layer-group'} />
|
||||
<span class="group-by-label">{active?.label ?? ''}</span>
|
||||
</button>
|
||||
{#if sortVisible}
|
||||
<button
|
||||
class="toggle-btn sort-dir-btn"
|
||||
class:active={reversed}
|
||||
title={t('sortdir.title', 'Sort direction')}
|
||||
aria-label={t('sort.direction', 'Sort direction')}
|
||||
data-testid="display-mode-sort-direction-btn"
|
||||
onclick={() => ondirection?.()}
|
||||
>
|
||||
<Icon name="arrow-up" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if menuOpen}
|
||||
<div class="group-by-menu">
|
||||
{#each groups as g (g.key)}
|
||||
<button
|
||||
class="group-by-option"
|
||||
class:active={groupBy === g.key}
|
||||
data-testid={`display-mode-groupby-${g.key}-item`}
|
||||
onclick={() => pick(g.key)}
|
||||
>
|
||||
<Icon name={g.icon ?? 'layer-group'} />
|
||||
{g.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showViewMode}<span class="view-toggle-separator"></span>{/if}
|
||||
{/if}
|
||||
{#if showViewMode}
|
||||
<button
|
||||
class="toggle-btn"
|
||||
class:active={filesStore.viewMode === 'grid'}
|
||||
title={t('view.grid', 'Grid view')}
|
||||
aria-pressed={filesStore.viewMode === 'grid'}
|
||||
data-testid="display-mode-view-grid-btn"
|
||||
onclick={() => filesStore.setViewMode('grid')}
|
||||
>
|
||||
<Icon name="th" />
|
||||
</button>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
class:active={filesStore.viewMode === 'list'}
|
||||
title={t('view.list', 'List view')}
|
||||
aria-pressed={filesStore.viewMode === 'list'}
|
||||
data-testid="display-mode-view-list-btn"
|
||||
onclick={() => filesStore.setViewMode('list')}
|
||||
>
|
||||
<Icon name="list" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if showDotfileToggle}
|
||||
<button
|
||||
class="toggle-btn"
|
||||
class:active={preferences.hideDotfiles}
|
||||
title={preferences.hideDotfiles
|
||||
? t('view.show_dotfiles', 'Show hidden files')
|
||||
: t('view.hide_dotfiles', 'Hide hidden files')}
|
||||
aria-pressed={preferences.hideDotfiles}
|
||||
data-testid="display-mode-dotfile-toggle-btn"
|
||||
onclick={() => preferences.toggleHideDotfiles()}
|
||||
>
|
||||
<Icon name={preferences.hideDotfiles ? 'eye-slash' : 'eye'} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -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<string>) => void;
|
||||
actions?: Snippet<[FileItem | FolderItem]>;
|
||||
toolbar?: Snippet;
|
||||
/** Batch toolbar shown when items are selected; receives selected items. */
|
||||
batchToolbar?: Snippet<[Array<FileItem | FolderItem>]>;
|
||||
/**
|
||||
* 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<FileItem | FolderItem>]>;
|
||||
/**
|
||||
* 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 `<img>` 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
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet row(item: FileItem | FolderItem)}
|
||||
@@ -644,6 +759,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
<span class="name-cell__text">{item.name}</span>
|
||||
{#if rowBadge}<span class="name-cell__badge">{@render rowBadge(item, ctx)}</span>{/if}
|
||||
</div>
|
||||
{#if showOwner}
|
||||
<div class="owner-cell">
|
||||
@@ -687,51 +803,83 @@
|
||||
}}><Icon name={isFav ? 'star' : 'star-outline'} /></button
|
||||
>
|
||||
{/if}
|
||||
{#if actions}
|
||||
<div class="action-cell">{@render actions(item)}</div>
|
||||
{#if itemActions}
|
||||
<div class="action-cell">{@render itemActions(item)}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!--
|
||||
`.rl-root` is the drop-target boundary for OS file drops. Every
|
||||
descendant listens through here — dragging in from the desktop over
|
||||
the sticky header or the content area both hit the same handler and
|
||||
either forward to the page's upload code (files browser) or fire the
|
||||
"wrong section" toast (everywhere else).
|
||||
|
||||
svelte-ignore a11y_no_static_element_interactions — the drop handlers
|
||||
are pointer-only fallbacks; keyboard users don't have a "drop a
|
||||
file" action, and the content inside is separately keyboard-nav'd.
|
||||
-->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="rl-root"
|
||||
class:rl-root--drop-over={systemDropOver && enableSystemDrop}
|
||||
ondragenter={onSystemDragEnter}
|
||||
ondragover={onSystemDragOver}
|
||||
ondragleave={onSystemDragLeave}
|
||||
ondrop={onSystemDrop}
|
||||
>
|
||||
<div class="page-sticky-header">
|
||||
<h1 class="page-title">{title}</h1>
|
||||
<ListToolbar
|
||||
groups={groupBys}
|
||||
{groupBy}
|
||||
{reversed}
|
||||
ongroup={selectGroup}
|
||||
ondirection={toggleDirection}
|
||||
{showViewToggle}
|
||||
{showDotfileToggle}
|
||||
>
|
||||
{#if breadcrumb}
|
||||
<div class="rl-breadcrumb">{@render breadcrumb()}</div>
|
||||
{/if}
|
||||
<ActionBar>
|
||||
{#snippet start()}
|
||||
<div class="action-buttons">{@render toolbar?.()}</div>
|
||||
<div class="action-buttons">
|
||||
<!--
|
||||
The action-bar left cluster has two states:
|
||||
1. `batchActions` — when the user has selected items, the
|
||||
page's batch buttons (Move / Delete / Restore / …)
|
||||
replace the default cluster, prefixed with a "clear
|
||||
selection" close button + count label so the batch is
|
||||
dismissable without unchecking every row by hand.
|
||||
2. `actions` — the page's default cluster
|
||||
(Upload / New folder / Empty trash / Clear recent).
|
||||
-->
|
||||
{#if selectable && selected.size > 0 && batchActions}
|
||||
<button
|
||||
class="rl-batch-close"
|
||||
title={t('common.clear', 'Clear selection')}
|
||||
aria-label={t('common.clear', 'Clear selection')}
|
||||
data-testid="resource-list-batch-close-btn"
|
||||
onclick={clearSelection}
|
||||
>
|
||||
<Icon name="times" />
|
||||
</button>
|
||||
<span class="rl-batch-count"
|
||||
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
|
||||
>
|
||||
{@render batchActions(selectedItems)}
|
||||
{:else if actions}
|
||||
{@render actions()}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ListToolbar>
|
||||
{#snippet end()}
|
||||
<DisplayModeControls
|
||||
groups={groupBys}
|
||||
{groupBy}
|
||||
{reversed}
|
||||
ongroup={selectGroup}
|
||||
ondirection={toggleDirection}
|
||||
showViewMode={showViewToggle}
|
||||
{showDotfileToggle}
|
||||
/>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
</div>
|
||||
|
||||
{#if selectable && selected.size > 0 && batchToolbar}
|
||||
<div
|
||||
class="rl-batch"
|
||||
role="region"
|
||||
aria-label={t('files.selection', 'Selection')}
|
||||
data-testid="resource-list-batch-toolbar"
|
||||
>
|
||||
<button
|
||||
class="rl-batch__close"
|
||||
title={t('common.clear', 'Clear')}
|
||||
data-testid="resource-list-batch-close-btn"
|
||||
onclick={clearSelection}
|
||||
>
|
||||
<Icon name="times" />
|
||||
</button>
|
||||
<span class="rl-batch__count"
|
||||
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
|
||||
>
|
||||
<div class="rl-batch__actions">{@render batchToolbar(selectedItems)}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<EmptyState icon="exclamation-circle" title={error} error />
|
||||
{:else if loading && isEmpty}
|
||||
@@ -822,6 +970,7 @@
|
||||
<div bind:this={sentinel} class="rl-sentinel" aria-hidden="true"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div><!-- /.rl-root -->
|
||||
|
||||
{#snippet listHeader()}
|
||||
<div class="list-header">
|
||||
@@ -842,7 +991,7 @@
|
||||
{#if showType}<div>{t('files.col_type', 'Type')}</div>{/if}
|
||||
{#if showSize}<div>{t('files.col_size', 'Size')}</div>{/if}
|
||||
{#if showDate}<div>{dateLabel ?? t('files.col_modified', 'Date')}</div>{/if}
|
||||
{#if onfavorite || actions}<div></div>{/if}
|
||||
{#if onfavorite || itemActions}<div></div>{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -889,19 +1038,47 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Batch toolbar ── */
|
||||
.rl-batch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
margin-bottom: var(--space-3);
|
||||
background: var(--color-accent-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
/* ── OS-drop wrapper ──
|
||||
`.rl-root` catches drops that miss a specific in-app drop target
|
||||
(row → folder). Its highlight fires ONLY when
|
||||
`enableSystemDrop && dragging` — the "wrong drop zone" toast path
|
||||
deliberately leaves the surface unhighlighted so users don't get a
|
||||
false accept cue. */
|
||||
.rl-root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rl-batch__close {
|
||||
.rl-root--drop-over::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 2px dashed var(--color-accent);
|
||||
border-radius: var(--radius-md);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Breadcrumb strip inside the sticky header ── */
|
||||
.rl-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
/* ── Row badge (trash expiration, etc.) ── */
|
||||
.name-cell__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
|
||||
/* ── Selection controls inside the action bar ──
|
||||
Replaces the deprecated `.rl-batch` floating strip. When items
|
||||
are selected, the close button + count sit before the page's
|
||||
`batchActions` snippet inside `.action-buttons`, so the whole
|
||||
cluster reads as one row of the action bar. */
|
||||
.rl-batch-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -914,22 +1091,15 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rl-batch__close:hover {
|
||||
.rl-batch-close:hover {
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.rl-batch__count {
|
||||
.rl-batch-count {
|
||||
font-weight: var(--weight-semibold);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.rl-batch__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── Selection column ── */
|
||||
.select-cell {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user