diff --git a/frontend/src/lib/components/ResourceList.svelte b/frontend/src/lib/components/ResourceList.svelte index 47d37647..8be57b8a 100644 --- a/frontend/src/lib/components/ResourceList.svelte +++ b/frontend/src/lib/components/ResourceList.svelte @@ -163,6 +163,17 @@ * the action is meaningful for and returns nothing otherwise. */ bucketAction?: Snippet<[string]>; + /** + * Optional custom renderer for the swimlane header label. Receives + * the bucket key + the default label string (from `labelOf`, or + * the key itself if no `labelOf`). Pages that want rich header + * content (e.g. `/shared-with-me` prefixing the "Shared by X" + * header with the sharer's avatar) use this; pages happy with + * a plain text label leave it undefined and ResourceList renders + * `{section.label}` as before. The default label is passed too + * so pages don't have to re-invoke `labelOf` themselves. + */ + bucketLabel?: Snippet<[string, string]>; /** Show the owner column + vignette (list view) and hover tooltip. */ showOwner?: boolean; /** @@ -248,6 +259,20 @@ * don't have to piggyback on the bar. */ itemActions?: Snippet<[FileItem | FolderItem]>; + /** + * Free-form overlay rendered inside `.file-item` (as a sibling of + * `.action-cell`), so the page can absolute-position content + * anywhere on the card. Used by `/shared-with-me` to anchor the + * sharer avatar at the bottom-right of the card — the pre-fix + * `rowBadge` slot rendered inside `.file-icon` (a positioned + * ancestor), which couldn't align with `.action-cell`'s + * `.file-item`-scoped coordinates. The page provides its own + * absolute-positioning CSS via a scoped style block. + * + * Fires in grid view only — list view has explicit columns + * (owner cell, etc.) for the same information. + */ + cardOverlay?: Snippet<[FileItem | FolderItem, ItemContext | undefined]>; /** * Action-bar left cluster — always-visible page action buttons * (Upload / New folder / Empty trash / Clear recent / …). Swaps @@ -385,6 +410,7 @@ dateLabel, dateCell, bucketAction, + bucketLabel, showOwner = false, ownerLabel, showViewToggle = true, @@ -402,6 +428,7 @@ oncontextmenu: onContextMenuOverride, menuPrepare, itemActions, + cardOverlay, actions, batchActions, rowBadge, @@ -1240,6 +1267,17 @@ {/if} {/if} + + {#if cardOverlay && filesStore.viewMode === 'grid'} + {@render cardOverlay(item, ctx)} + {/if} {/snippet} @@ -1370,7 +1408,13 @@ {#each sections as section (section.key)} {#if section.label}
- {section.label} + + {#if bucketLabel} + {@render bucketLabel(section.key, section.label)} + {:else} + {section.label} + {/if} + {#if bucketAction} {@render bucketAction(section.key)} @@ -1395,7 +1439,13 @@ {#each sections as section (section.key)} {#if section.label}
- {section.label} + + {#if bucketLabel} + {@render bucketLabel(section.key, section.label)} + {:else} + {section.label} + {/if} + {#if bucketAction} {@render bucketAction(section.key)} @@ -1680,6 +1730,17 @@ align-items: center; } + /* Label slot — inline-flex + gap so pages injecting rich content + via the `bucketLabel` snippet (e.g. `/shared-with-me` prefixing + with a sharer avatar) render avatar-then-text on one baseline + without hand-tuned spacing. Plain-text labels (no snippet) still + look identical — flex on a single text node is a no-op. */ + .rl-swimlane-header__label { + display: inline-flex; + align-items: center; + gap: var(--space-2); + } + /* Grouped-grid container: a vertical stack of (header + its own windowed card grid) per section. Not `.files-grid-view` — the grid is on each VirtualList's inner window, so this outer element just stacks. */ diff --git a/frontend/src/lib/components/UserAvatar.svelte b/frontend/src/lib/components/UserAvatar.svelte new file mode 100644 index 00000000..c346a4bb --- /dev/null +++ b/frontend/src/lib/components/UserAvatar.svelte @@ -0,0 +1,131 @@ + + + + {#if image} + + {:else} + + {initials} + + {/if} + + + diff --git a/frontend/src/lib/styles/ported/resourceList.css b/frontend/src/lib/styles/ported/resourceList.css index b30b3d18..00e3d80d 100644 --- a/frontend/src/lib/styles/ported/resourceList.css +++ b/frontend/src/lib/styles/ported/resourceList.css @@ -360,6 +360,19 @@ white-space: nowrap; } +/* List-view rows are fixed 56px (VirtualList `rowHeight={56}`), and a + full UserVignette stacks avatar (32px) + name (20px) + email (15px) + → ~40px block that doesn't visually fit the flex-centered cell — + the email tail was cropped. Hide the email in dense-row contexts + (Ed 2026-07-26); the identity signal remains (avatar + name), and + the email survives in the ShareDialog / recipient pickers where + UserVignette was originally scaled for. Grid view's owner cell is + unaffected (it never renders a UserVignette — the sharer surfaces + through the `.file-icon__badge` slot instead). */ +.files-list-view .file-item .owner-cell .uv__email { + display: none; +} + /* Expand the grid track as soon as at least one owner cell is visible. */ .files-list-view:has(.owner-cell:not(.hidden)) { --files-list-columns: 36px minmax(200px, 2fr) 120px 100px 110px 130px 200px; @@ -409,7 +422,10 @@ overflow: hidden; } -.files-list-view .file-item .name-cell span { +/* Same narrowing rationale as the grid view rule below — target only + the name text span, not every descendant span, so `.file-icon__badge` + overlay contents (avatars, chips) aren't sized as text. */ +.files-list-view .file-item .name-cell__text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -1036,11 +1052,16 @@ margin-top: var(--space-1); } -.files-grid-view .file-item .name-cell span { +/* Narrowed from the pre-fix `.name-cell span` (descendant selector) to + the specific name-text span. The broad rule caught EVERY span inside + `.name-cell` — including the `.file-icon__badge` overlay and, inside + it, ``'s `.ua` wrapper — and slapped 8 px of top/bottom + padding on them, which crushed the badge's `` from 22 × 22 down + to 22 × 6 (Ed 2026-07-26). Text ellipsis / padding stays on the text + span alone; overlay children in `.file-icon` are unaffected. */ +.files-grid-view .file-item .name-cell__text { display: block; max-width: 100%; - /* Ellipsis must live on the span (the text node), not the flex parent, - or long names clip with no "…". */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/frontend/src/routes/shared-with-me/+page.svelte b/frontend/src/routes/shared-with-me/+page.svelte index 2d327846..4dbcd55d 100644 --- a/frontend/src/routes/shared-with-me/+page.svelte +++ b/frontend/src/routes/shared-with-me/+page.svelte @@ -24,6 +24,7 @@ type GroupByDef, type ItemContext } from '$lib/components/ResourceList.svelte'; + import UserAvatar from '$lib/components/UserAvatar.svelte'; import { t } from '$lib/i18n/index.svelte'; import { session } from '$lib/stores/session.svelte'; @@ -259,7 +260,43 @@ cursor = undefined; load(true, orderBy, rev); }} -/> +> + {#snippet cardOverlay(_item, ctx)} + + {#if ctx?.ownerId} + + + + {/if} + {/snippet} + {#snippet bucketLabel(_key, label)} + + {#if groupBy === 'sharedBy' && _key} + + {/if} + {label} + {/snippet} + {#if fileViewer.component} {@const FileViewer = fileViewer.component} @@ -267,6 +304,35 @@ {/if}