Files
Oxicloud/frontend/src/lib/styles/ported/resourceList.css
T

1306 lines
43 KiB
CSS
Raw Normal View History

2026-06-17 17:06:30 -06:00
/* ============================================================
* ResourceList component styles
*
* All styles for .file-item (both grid cards and list rows),
* the list header, drag ghost, per-item checkboxes, and
* section-specific item modifiers (.favorite-item, .recent-item,
* .trash-item).
*
* Page-level shell (.files-container, .selection-rect) → fileManager.css
* Batch-action toolbar (.batch-selection-bar, .batch-btn …) → multiSelect.css
* Section page headers (.list-header.favorites-header …) → views/*.css
* ============================================================ */
/* ── Base icon container ─────────────────────────────────── */
.file-icon {
width: 100px;
height: 70px;
border-radius: var(--radius-lg);
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* Thumbnail image inside file-icon (grid and list views) */
.file-icon .file-thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
/* The thumbnail is purely decorative — it must never be a hit-test target.
In grid view it fills the whole tile UNDER the overlay controls (checkbox,
favorite star, kebab); a loaded full-bleed thumbnail (promoted to its own
stacking context by the hover scale transform) was capturing clicks meant
for those controls, so they "did nothing" (the click fell through to
open-file). Making it transparent to pointer events routes corner clicks
to the controls and body clicks to the .file-icon (which still opens). */
pointer-events: none;
}
/* ── File-type colours ───────────────────────────────────────
Each bucket sets a local --fk (the foreground hue, from the
--file-kind-* tokens). The glyph takes --fk directly and the tile
fill/ring derive soft tints from it with color-mix(), so the icons read
as colourful tiles instead of flat monochrome. Adding a bucket is a
single custom-property line; the consuming rules never change. */
.file-icon {
--fk: var(--file-kind-generic);
}
.file-icon--folder {
--fk: var(--file-kind-folder);
}
.file-icon--pdf {
--fk: var(--file-kind-pdf);
}
.file-icon--doc {
--fk: var(--file-kind-doc);
}
.file-icon--sheet {
--fk: var(--file-kind-sheet);
}
.file-icon--slides {
--fk: var(--file-kind-slides);
}
.file-icon--archive {
--fk: var(--file-kind-archive);
}
.file-icon--code {
--fk: var(--file-kind-code);
}
.file-icon--image {
--fk: var(--file-kind-image);
}
.file-icon--video {
--fk: var(--file-kind-video);
}
.file-icon--audio {
--fk: var(--file-kind-audio);
}
.file-icon--text {
--fk: var(--file-kind-text);
}
.file-icon--generic {
--fk: var(--file-kind-generic);
}
.file-icon > i,
.file-icon > svg {
color: var(--fk);
}
2026-06-17 17:06:30 -06:00
/* ── Item states ─────────────────────────────────────────── */
.file-item {
background-color: var(--color-item);
/* Smooth the hover/selection tint (list rows used to snap). Grid cards
override this with their own transform/shadow transition. */
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard);
}
/* Brief pulse on rows that were just optimistically inserted (e.g.
newly created folder, upload completion, drag-drop move into the
current folder). Pure CSS so timing is deterministic — the JS adds
the class, the animation auto-clears the background, and a
single `animationend` listener removes the class.
`scroll-margin-top` reserves space above the row for the sticky
page header (`.page-sticky-header` ≈ 80 px). Without it,
`scrollIntoView({ block: 'nearest' })` aligns the row's top edge
against the viewport's top edge — which the sticky header is
currently covering — so the user only sees the row's bottom edge.
`scroll-margin-bottom` gives a touch of breathing room when the
scroll happens to land the row near the viewport bottom. */
.file-item.resource-row--just-added {
animation: resource-row-just-added 1.5s ease-out;
scroll-margin-top: 100px;
scroll-margin-bottom: 24px;
}
@keyframes resource-row-just-added {
0% {
background-color: var(--color-success-bg);
}
100% {
background-color: transparent;
}
}
/* Client-only "New" swimlane created on the fly by `addItem()` when
the view is grouped. Subtler styling than a natural-group lane: the
user understands the pin is temporary (it dissolves on next full
reload), so we don't want the bar to dominate the list. The pinned
placement at the top of the container is what makes it
discoverable; the header just confirms the intent. */
.resource-list__swimlane-group--just-added > .resource-list__swimlane-header {
color: var(--color-success-text);
}
.file-item.selected {
background-color: var(--color-item-selected);
}
.file-item:hover {
background-color: var(--color-item-hover);
border-color: var(--color-border-medium);
}
.file-item.selected:hover {
background-color: var(--color-item-hover-accent);
}
.file-item.dragging {
opacity: 0.8;
background-color: var(--color-bg-input);
}
.file-item.drop-target {
background-color: var(--color-warning-ring);
}
.file-item .file-icon > i,
.file-item .file-icon > svg {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Per-item checkboxes (list + grid) ───────────────────── */
.list-header-checkbox,
.file-item .checkbox-cell {
display: flex;
align-items: center;
justify-content: center;
}
.list-header-checkbox input[type="checkbox"],
.file-item .checkbox-cell input[type="checkbox"] {
width: 17px;
height: 17px;
cursor: pointer;
accent-color: var(--color-accent);
border-radius: var(--radius-sm);
}
.list-header.selection-mode {
grid-template-columns: 36px 1fr;
background-color: var(--color-multiselect-bg);
color: var(--color-multiselect-text);
border-bottom-color: var(--color-multiselect-border);
}
.list-header.selection-mode .list-header-checkbox input[type="checkbox"] {
accent-color: var(--color-accent);
}
/* ── List view ───────────────────────────────────────────── */
.list-header {
display: grid;
grid-template-columns: var(--files-list-columns);
column-gap: var(--space-3);
padding: 15px;
font-weight: var(--weight-semibold);
color: var(--color-text);
background-color: var(--color-bg-subtle);
border-bottom: 1px solid var(--color-border-faint);
align-items: center;
}
/* Trash view: Name → [Path] → Size → Date → Actions
* Override --files-list-columns so the trash header AND trash items align.
* No checkbox (selectable=false), no owner cell visible, no type column.
*
* Mobile-first: the Path column is hidden by default to keep the layout
* legible on narrow screens (path stays accessible via itemTooltip on hover).
* From 1000px upward, Path reappears and claims roughly half the table
* width via a 3fr share against Name's 1fr. */
.files-list-view.trash-list {
--files-list-columns: minmax(180px, 1fr) 110px 130px 100px;
}
.files-list-view.trash-list .file-item .path-cell,
.files-list-view.trash-list .list-header.trash-header > div:nth-child(2) {
display: none;
}
@media (min-width: 1000px) {
.files-list-view.trash-list {
--files-list-columns: minmax(180px, 1fr) 3fr 110px 130px 100px;
}
.files-list-view.trash-list .file-item .path-cell,
.files-list-view.trash-list .list-header.trash-header > div:nth-child(2) {
display: block;
}
}
.list-header > div,
.files-list-view .file-item > div {
min-width: 0;
}
2026-07-20 21:16:41 +02:00
/* Column alignment — targets classes on BOTH the header divs AND the value
cells, so the header label always matches its column's value alignment
regardless of which optional columns (path/type/owner/…) are on. The
previous shape keyed off `nth-child(N)` and drifted the moment a
ResourceList caller toggled a `show*` prop. */
.list-header > .size-cell,
2026-06-17 17:06:30 -06:00
.files-list-view .file-item .size-cell {
justify-self: end;
text-align: right;
}
/* ── Sortable column headers (flat Drive-style sort) ─────────── */
.list-header-sort {
display: inline-flex;
align-items: center;
gap: var(--space-1-5);
min-width: 0;
padding: 0;
border: none;
background: none;
cursor: pointer;
/* Inherit the header row's weight + colour so it reads as a header. */
font: inherit;
color: inherit;
transition: color var(--motion-fast) var(--ease-standard);
}
.list-header-sort:hover,
.list-header-sort.is-active {
color: var(--color-accent);
}
.list-header-sort__arrow {
font-size: var(--text-2xs);
color: var(--color-accent);
flex-shrink: 0;
}
/* Match the value-cell alignment so header and data line up. */
.list-header-sort[data-sort-field="size"] {
justify-self: end;
}
.list-header-sort[data-sort-field="modified_at"] {
justify-self: center;
}
/* ── Owner column ────────────────────────────────────────── */
/* Styles applied whenever the cell is visible (hidden class absent).
The .hidden utility class (display:none !important) keeps it invisible
by default — it is stamped directly in the HTML templates.
`display: flex` lets a nested `.user-vignette` (the common case
for an owner cell — an avatar circle plus a name) become a flex
child that can shrink past its intrinsic content width. The
`.user-vignette__name` already declares
`text-overflow: ellipsis`, but that only fires when the cascade
above it ACTUALLY constrains the width. Without flex here, the
vignette sized to its content and the cell clipped it flat with
no ellipsis. The cell's own `text-overflow` still ellipses
plain-text fallback content (cells without a vignette child). */
2026-07-20 21:16:41 +02:00
/* Scoped to `.file-item` so the header div — which also carries the
`.owner-cell` class now (so column-alignment CSS keys off classes
instead of brittle nth-child indices) — doesn't inherit the muted
cell colour / cell font size. Header keeps `.list-header`'s
semibold + text colour. */
.file-item .owner-cell {
2026-06-17 17:06:30 -06:00
color: var(--color-text-secondary);
font-size: var(--text-base);
display: flex;
align-items: center;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Expand the grid track as soon as at least one owner cell is visible. */
.files-list-view:has(.owner-cell:not(.hidden)) {
2026-06-20 19:13:18 +02:00
--files-list-columns: 36px minmax(200px, 2fr) 120px 100px 110px 130px 200px;
2026-06-17 17:06:30 -06:00
}
.files-list-view {
2026-06-20 19:13:18 +02:00
--files-list-columns: 36px minmax(200px, 2fr) 100px 110px 130px 200px;
2026-06-17 17:06:30 -06:00
display: flex;
flex-direction: column;
width: 100%;
border-radius: var(--radius-xl);
overflow: hidden;
background-color: var(--color-item);
box-shadow: 0 1px 3px var(--color-shadow-xs);
}
.files-list-view .file-item {
display: grid;
grid-template-columns: var(--files-list-columns);
column-gap: var(--space-3);
padding: var(--space-3) 15px;
border-bottom: 1px solid var(--color-border-xfaint);
align-items: center;
cursor: pointer;
transition:
background-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard);
}
/* "This row" emphasis — a left accent bar on hover/selection (inset shadow,
so it adds no width and never shifts the grid columns). */
.files-list-view .file-item:hover,
.files-list-view .file-item.selected {
box-shadow: inset 3px 0 0 var(--color-accent);
}
.files-list-view .file-item.drop-target {
border: 1px dashed var(--color-warning-border);
}
.files-list-view .file-item .name-cell {
color: var(--color-text);
display: flex;
align-items: center;
gap: var(--space-3);
min-width: 0;
overflow: hidden;
}
.files-list-view .file-item .name-cell span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Content-search snippet — fragment of the file body that matched the
query. Wraps onto a second line inside the flex name-cell. */
.files-list-view .file-item .name-cell:has(.file-item__snippet) {
flex-wrap: wrap;
row-gap: 2px;
}
.files-list-view .file-item .file-item__snippet {
flex-basis: 100%;
/* icon width (36px) + cell gap (12px) — aligns under the name */
padding-left: 48px;
font-size: 12px;
color: var(--color-text-muted);
}
/* Grid cards are too compact for body fragments. */
.files-grid-view .file-item .file-item__snippet {
display: none;
}
.files-list-view .file-item .file-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
/* Soft type-tinted tile + matching hairline ring so the row glyph reads as
a colourful chip (and light previews don't bleed into the row). */
background: color-mix(in srgb, var(--fk) 12%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fk) 22%, var(--color-border));
2026-06-17 17:06:30 -06:00
font-size: var(--text-md);
margin-bottom: 0;
flex-shrink: 0;
}
2026-07-20 21:16:41 +02:00
.list-header > .date-cell,
2026-06-17 17:06:30 -06:00
.files-list-view .file-item .date-cell {
justify-self: center;
text-align: center;
}
.files-list-view .file-item .date-cell {
color: var(--color-text-muted);
font-size: var(--text-base);
}
.files-list-view .file-item .size-cell {
color: var(--color-text-muted);
font-size: var(--text-base);
text-align: right;
font-variant-numeric: tabular-nums;
}
.files-list-view .file-item .type-cell {
color: var(--color-text-secondary);
font-weight: var(--weight-medium);
font-size: var(--text-base);
}
.files-list-view .file-item .action-cell {
/* Right-justified flex row: gives every list-view action button
(shared, fav, broom, kebab, trash's restore/delete, …) a
predictable horizontal cluster with a stable gap. The legacy
`display: inline` on individual buttons ignored `width`/`height`,
so once /recent grew a 4th button the mix of `inline` + `inline-
flex` (`.btn-action`) started wrapping onto pseudo-rows that
read as a broken grid. Flex flattens the mix into one line. */
display: flex;
justify-content: flex-end;
2026-06-17 17:06:30 -06:00
align-items: center;
gap: var(--space-1);
2026-06-17 17:06:30 -06:00
text-align: right;
}
2026-06-17 22:07:18 -06:00
/* List rows expose the individual action buttons inline (on hover), so the
corner kebab is redundant here — it's the grid view's affordance. */
.files-list-view .file-item .action-cell .file-actions {
display: none;
}
2026-06-17 17:06:30 -06:00
/* Styles for the built-in action-cell buttons (favorite-star, kebab).
* `.btn-action` is excluded — it owns its own colors via the generic
* `.btn-action` rule + variant modifiers (e.g. `.btn-action--delete`). */
.files-list-view .file-item .action-cell button:not(.btn-action),
.files-list-view .file-item .action-cell div {
display: inline;
width: 28px;
height: 28px;
border-radius: var(--radius-lg);
border: none;
background: transparent;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--color-text-subtle);
font-size: var(--text-md);
}
.files-list-view .file-item .action-cell button:not(.btn-action):hover {
background: var(--color-border-subtle);
color: var(--color-text-dark);
}
/* Fav-star + shared-button share the same visibility rule: hidden on
quiet rows, visible on row hover, and — crucially — always visible
when their `.active` class is set. That's what lets a favorited or
shared row be discoverable at a glance in list view without the
user having to mouse over it.
We use `visibility: hidden` (not `display: none`) so a hidden
button still reserves its slot in the action cell. Otherwise a row
that's shared-but-not-favorited would slide its shared icon into
the fav-star's column, breaking vertical alignment across rows. */
.files-list-view .file-item .action-cell button.favorite-star,
.files-list-view .file-item .action-cell button.shared-button {
visibility: hidden;
2026-06-17 17:06:30 -06:00
border: none;
}
.files-list-view .file-item:hover .action-cell button.favorite-star,
.files-list-view .file-item:hover .action-cell button.shared-button,
.files-list-view .file-item .action-cell button.favorite-star.active,
.files-list-view .file-item .action-cell button.shared-button.active {
visibility: visible;
2026-06-17 17:06:30 -06:00
}
/* Reveal the kebab on hover for cleaner rows — but only on hover-capable
devices, so touch users (no hover) keep it always tappable. Stays visible
on keyboard focus within the row. Applies to both list and grid views
because both keep the kebab inside `.action-cell`. */
2026-06-17 17:06:30 -06:00
@media (hover: hover) {
.files-list-view .file-item .action-cell button.file-actions,
.files-grid-view .file-item .action-cell button.file-actions {
2026-06-17 17:06:30 -06:00
opacity: 0;
transition: opacity var(--motion-fast) var(--ease-standard);
}
.files-list-view .file-item:hover .action-cell button.file-actions,
.files-list-view .file-item:focus-within .action-cell button.file-actions,
.files-grid-view .file-item:hover .action-cell button.file-actions,
.files-grid-view .file-item:focus-within .action-cell button.file-actions {
2026-06-17 17:06:30 -06:00
opacity: 1;
}
}
/* ── Grid view ───────────────────────────────────────────── */
.files-grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--grid-card-min), 1fr));
gap: var(--space-5);
position: relative;
}
/* header not visible in grid mode */
.files-grid-view .list-header {
display: none;
}
/* ── Mobile reflow (≤ --bp-sm 640px) ──────────────────────────
The 6-column grid can't fit a phone, so each list row collapses to a
compact flex line (icon + name … size + actions) and the column header
is hidden (sort controls live in the actions bar). One block covers
files / trash / favorites / recent uniformly; flex ignores the grid
tracks, so there's no header-vs-row misalignment to maintain. */
@media (max-width: 640px) {
/* Keep the selection-mode header (holds select-all); hide the plain one. */
.list-header:not(.selection-mode) {
display: none;
}
.files-list-view .file-item {
display: flex;
align-items: center;
gap: var(--space-3);
}
.files-list-view .file-item .type-cell,
.files-list-view .file-item .date-cell,
.files-list-view .file-item .owner-cell,
.files-list-view .file-item .path-cell {
display: none;
}
.files-list-view .file-item .name-cell {
flex: 1;
min-width: 0;
}
.files-list-view .file-item .checkbox-cell,
.files-list-view .file-item .size-cell,
.files-list-view .file-item .action-cell {
flex-shrink: 0;
}
/* Tighter grid on phones (2 columns instead of 1). */
.files-grid-view {
--grid-card-min: 140px;
gap: var(--space-2);
}
}
.files-grid-view .file-item {
border-radius: var(--radius-2xl);
/* Hairline at rest → accent on hover (gallery, not form). */
border: 1px solid var(--color-border);
padding: var(--space-3);
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 1px 2px var(--color-shadow-xs);
cursor: pointer;
width: 100%;
min-height: 160px;
position: relative;
transition:
transform var(--motion-base) var(--ease-standard),
box-shadow var(--motion-base) var(--ease-standard),
border-color var(--motion-base) var(--ease-standard);
}
.files-grid-view .file-item.dragging {
opacity: 0.5;
transform: scale(0.95);
box-shadow: none;
}
.files-grid-view .file-item:hover {
transform: translateY(-3px);
box-shadow: 0 12px 28px -8px var(--color-shadow-md);
border-color: var(--color-accent);
}
/* The thumbnail subtly zooms inside its clipped tile on hover — the "alive"
feel of a premium gallery. */
.files-grid-view .file-item .file-icon .file-thumb {
transition: transform var(--motion-moderate) var(--ease-standard);
}
.files-grid-view .file-item:hover .file-icon .file-thumb {
transform: scale(1.04);
}
.files-grid-view .file-item.selected {
border-color: var(--color-accent);
box-shadow:
0 0 0 1px var(--color-accent-ring-dark),
0 4px 12px var(--color-accent-ring);
}
.files-grid-view .file-item.selected:hover {
box-shadow:
0 0 0 1px var(--color-accent-ring-strong),
0 6px 18px var(--color-accent-ring-dark);
}
/* elements hidden in grid view — the type label ("Imagen") is redundant under
a thumbnail, so we drop it and surface the file size instead. */
.files-grid-view .file-item .date-cell,
.files-grid-view .file-item .type-cell,
.files-grid-view .file-item .owner-cell {
display: none;
}
/* Selection checkbox */
/* Overlay controls sit ON the thumbnail (offset by the card padding) over a
frosted scrim, so they read as part of the media instead of floating on the
card corner. */
.files-grid-view .file-item .checkbox-cell {
position: absolute;
top: calc(var(--space-3) + 8px);
left: calc(var(--space-3) + 8px);
width: 26px;
height: 26px;
border-radius: var(--radius-md);
background: var(--color-scrim-control);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
box-shadow: 0 1px 3px var(--color-shadow-sm);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
z-index: 10;
cursor: pointer;
transition: opacity var(--motion-fast) var(--ease-standard);
}
/* Visible whenever the card is hovered OR already selected, so the checked
state stays on screen after the pointer leaves. */
.files-grid-view .file-item:hover .checkbox-cell,
.files-grid-view .file-item.selected .checkbox-cell {
opacity: 1;
}
/* Custom-drawn checkbox: an empty white box that fills with accent and shows a
crisp white tick when checked. The native 17px control was invisible once the
cell turned accent on selection (accent-on-accent), and the intended tick
targeted a `.checkbox-cell i` the markup never renders — so a click read as
"nothing happened". This makes the checked state unmistakable. */
.files-grid-view .file-item .checkbox-cell input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 18px;
height: 18px;
margin: 0;
border: 2px solid var(--color-border-medium);
border-radius: var(--radius-sm);
background: var(--color-bg-surface);
display: grid;
place-content: center;
cursor: pointer;
transition:
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard);
}
.files-grid-view .file-item .checkbox-cell input[type="checkbox"]::after {
content: "";
width: 5px;
height: 9px;
margin-top: -1px;
border: solid var(--color-danger-text);
border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0);
transform-origin: center;
transition: transform var(--motion-fast) var(--ease-standard);
}
.files-grid-view .file-item .checkbox-cell input[type="checkbox"]:checked {
background: var(--color-accent);
border-color: var(--color-accent);
}
.files-grid-view .file-item .checkbox-cell input[type="checkbox"]:checked::after {
transform: rotate(45deg) scale(1);
}
.files-grid-view .file-item .checkbox-cell input[type="checkbox"]:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* ── Grid card action cluster ─────────────────────────────────────
Every action a row can surface — favorite star, `.file-actions`
kebab, per-section `.btn-action` icons (e.g. trash's Restore /
Delete permanently) — lives in a single `.action-cell` container
pinned to the top-right of the card. The container carries the
position + hover-reveal + gap; its children just supply their
own chip visuals (30x30 scrim pill, etc.), no more one-off
absolute positioning per child.
Old rules put `.file-actions` and `.favorite-star` at hand-crafted
absolute coordinates and hid `.btn-action` entirely — that made
trash's per-item buttons invisible in grid view. The unified
container reads as one design pattern and takes whatever children
the row template hands it. */
.files-grid-view .file-item .action-cell {
position: absolute;
top: calc(var(--space-3) + 8px);
right: calc(var(--space-3) + 8px);
z-index: 10;
display: flex;
gap: var(--space-1);
}
/* Full-width top action bar — only applies when the row surfaces at
least one state chip (shared or favorite). Trash (no shared / no
favorite) falls through to the horizontal corner cluster above,
preserving its Restore / Delete / kebab layout.
Layout target:
┌────────────────────────────────────────────┐
│[chk] [kebab] [btn-action…] [shared] [fav] │ ← single row
│ │
│ (thumbnail) │
│ │
└────────────────────────────────────────────┘
The checkbox stays where it is (its own `.checkbox-cell` absolute at
top-left); the action-cell spans horizontally next to it so the two
clusters read as one row. `justify-content: space-between` + `order`
splits the flex row into a left group (kebab + btn-actions) and a
right group (shared + favorite) without fighting DOM order — the
markup still has shared, favorite, itemActions, kebab in that
sequence so list-view's inline right-aligned flow is unchanged. */
.files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) {
/* Start right after the checkbox column (26px chip + inline gap)
so the left group aligns visually with the checkbox row. */
left: calc(var(--space-3) + 8px + 26px + var(--space-2));
right: calc(var(--space-3) + 8px);
display: flex;
align-items: center;
justify-content: flex-start;
gap: var(--space-1);
2026-06-17 22:07:18 -06:00
}
.files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .file-actions {
order: 1;
}
.files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .btn-action {
order: 2;
}
/* The first "right group" item eats all remaining horizontal space via
`margin-left: auto`, which is the flexbox idiom for splitting a row
into left+right clusters without wrapping the two groups in extra
containers. When both buttons are wired, shared is first (`order: 3`)
and takes the push; fav follows with the normal gap. When only fav
is wired (`/shared-with-me` — recipient side, no share affordance),
fav is the sole right-group item and takes the push instead. */
.files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .shared-button {
order: 3;
margin-left: auto;
}
.files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .favorite-star {
order: 4;
}
.files-grid-view
.file-item
.action-cell:has(.favorite-star):not(:has(.shared-button))
.favorite-star {
margin-left: auto;
}
/* Per-button visibility (mirrors list view): each button is independently
gated by its own `.active` flag OR row-hover. This prevents a favorited
row from also lighting up the shared button (and vice versa) — the cell
used to reveal all its children together via a single opacity toggle. */
.files-grid-view .file-item .action-cell .favorite-star,
.files-grid-view .file-item .action-cell .shared-button {
visibility: hidden;
transition: visibility var(--motion-fast) var(--ease-standard);
}
.files-grid-view .file-item:hover .action-cell .favorite-star,
.files-grid-view .file-item:hover .action-cell .shared-button,
.files-grid-view .file-item:focus-within .action-cell .favorite-star,
.files-grid-view .file-item:focus-within .action-cell .shared-button,
.files-grid-view .file-item .action-cell .favorite-star.active,
.files-grid-view .file-item .action-cell .shared-button.active {
visibility: visible;
2026-06-17 22:07:18 -06:00
}
/* Chip visuals for anything inside the corner cluster — the kebab, the star,
the shared button, any `.btn-action`. Uniform 30x30 scrim pill so they
line up in the flex row. */
.files-grid-view .file-item .action-cell .file-actions,
.files-grid-view .file-item .action-cell .favorite-star,
.files-grid-view .file-item .action-cell .shared-button,
.files-grid-view .file-item .action-cell .btn-action {
position: static;
2026-06-17 17:06:30 -06:00
width: 30px;
height: 30px;
/* `margin: 0` overrides the legacy `.files-grid-view .file-item
.btn-action { margin-top: var(--space-1) }` rule further down —
inside the corner cluster the parent's `gap` handles spacing
and any per-child margin would misalign the pills. */
margin: 0;
padding: 0;
2026-06-17 17:06:30 -06:00
border: none;
border-radius: var(--radius-full);
2026-06-17 17:06:30 -06:00
background: var(--color-scrim-control);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
box-shadow: 0 1px 3px var(--color-shadow-sm);
display: inline-flex;
2026-06-17 17:06:30 -06:00
align-items: center;
justify-content: center;
color: var(--color-text);
font-size: var(--text-md);
cursor: pointer;
/* Opacity/hover-reveal moves up to `.action-cell` — children stay opaque. */
2026-06-17 17:06:30 -06:00
opacity: 1;
}
.files-grid-view .file-item .action-cell .file-actions:hover {
2026-06-17 17:06:30 -06:00
color: var(--color-accent);
}
.files-grid-view .file-item.drop-target {
background-color: var(--color-warning-ring);
border: 2px dashed var(--color-warning-border);
}
/* Favorite star + shared button — visual overrides only. Position,
hover-reveal, chip geometry all come from the shared corner-cluster
rule on `.files-grid-view .file-item .action-cell`. What's left
here is just the per-state colour: subtle at rest, saturated when
the item's flag is set. `.active` on either button also bumps the
parent cluster's opacity (via `:has()` above) so an unhovered card
still shows its favorited/shared state. */
.files-grid-view .file-item button.favorite-star,
.files-grid-view .file-item button.shared-button {
2026-06-17 17:06:30 -06:00
color: var(--color-text-subtle);
font-size: 15px;
line-height: var(--leading-none);
}
.files-grid-view .file-item button.favorite-star:hover {
color: var(--color-star-text);
}
.files-grid-view .file-item button.favorite-star.active {
color: var(--color-star-text-hover);
}
.files-grid-view .file-item button.favorite-star.active:hover {
color: var(--color-star-active);
}
.files-grid-view .file-item button.shared-button:hover,
.files-grid-view .file-item button.shared-button.active {
color: var(--color-badge-blue-text);
}
2026-06-17 17:06:30 -06:00
.files-grid-view .file-item .name-cell {
font-size: var(--text-sm);
font-weight: var(--weight-medium);
text-align: center;
margin-bottom: 2px;
color: var(--color-text);
/* Span the full card width. The card is a centered flex column
(`align-items: center`), so without an explicit width the name-cell —
which wraps BOTH the thumbnail tile and the filename — shrink-wraps to
the filename's length. That made `.file-icon { width: 100% }` track the
NAME width, so a long name ("CURSO TERRAFORM") rendered a big tile and a
short one ("Idiomas") a small one. Forcing 100% makes every thumbnail
tile identical regardless of name length. */
width: 100%;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: var(--space-1);
}
.files-grid-view .file-item .name-cell span {
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;
padding-top: var(--space-2);
padding-bottom: var(--space-2);
}
.files-grid-view .file-item .name-cell svg.favorite-star-inline {
display: none;
}
/* The grid card's secondary line is now the combined .grid-meta block
("hace 2 días · 4,31 MB"), so the standalone size-cell is hidden here. */
.files-grid-view .file-item .size-cell {
display: none;
}
/* ── Grid metadata line — recency + size (+ owner avatar when shared) ──────
Replaces the lone "4,31 MB": a top file app captions a thumbnail with WHEN
it was touched (the dominant retrieval cue), not just bytes. Hidden
everywhere except the grid — list view keeps its own date/size/owner
columns, so a display:none here drops it cleanly out of the row grid. */
.grid-meta {
display: none;
}
.files-grid-view .file-item .grid-meta {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
max-width: 100%;
margin-top: 2px;
font-size: var(--text-2xs);
color: var(--color-text-muted);
line-height: var(--leading-snug);
}
/* On a narrow card the relative date truncates first; the size never clips. */
.files-grid-view .file-item .grid-meta__date {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.files-grid-view .file-item .grid-meta__size {
flex: 0 0 auto;
color: var(--color-text-faint);
}
.files-grid-view .file-item .grid-meta__size::before {
content: "·";
margin-right: var(--space-1);
color: var(--color-text-faint);
}
/* Owner avatar — only rendered when the item is shared; sits at the line end. */
.files-grid-view .file-item .grid-meta .user-vignette {
flex: 0 0 auto;
margin-left: var(--space-1);
}
/* Full-width thumbnail tile with a fixed aspect ratio — the image
(`.file-thumb`, object-fit:cover) fills it edge-to-edge for a uniform,
rich grid instead of a small letterboxed preview. Non-image files show
their type icon centred on the placeholder fill. */
.files-grid-view .file-item .file-icon {
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
border-radius: var(--radius-lg);
/* Soft type-tinted fill + matching hairline ring (a thumbnail, when
present, covers both edge-to-edge). Gives non-preview files a modern
colourful tile keyed to their type instead of a flat grey panel. */
background: color-mix(in srgb, var(--fk) 9%, var(--color-bg-input));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fk) 22%, var(--color-border));
2026-06-17 17:06:30 -06:00
margin: 0 0 var(--space-3);
font-size: 30px;
}
/* Type icon for non-thumbnail files (documents, etc.) — perfectly centered in
the tile. `inset: 0` + `margin: auto` + a fixed size is the absolute-centering
trick; the previous `top: auto` broke the vertical half, bottom-anchoring the
icon so it sat low in the tile. */
.files-grid-view .file-item .file-icon > i,
.files-grid-view .file-item .file-icon > svg {
inset: 0;
margin: auto;
width: 56px;
height: 56px;
}
/* ── Drag ghost ──────────────────────────────────────────── */
.dragged-items {
--dragged-files-list-columns: 36px minmax(200px, 1fr);
flex-direction: column;
background-color: transparent;
overflow: hidden;
border-radius: var(--radius-xl);
width: 100%;
}
.dragged-items .file-item {
grid-template-columns: var(--dragged-files-list-columns);
column-gap: var(--space-3);
align-items: center;
background-color: var(--color-item);
display: flex;
padding: var(--space-1);
width: 360px;
height: 46px;
color: var(--color-text);
}
/* file name text is truncated */
.dragged-items .file-item div:nth-child(2) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dragged-items .file-item .file-icon {
width: 36px;
height: 36px;
}
.dragged-items div.fading {
-webkit-mask-image: linear-gradient(to bottom, var(--color-item) 10%, transparent);
mask-image: linear-gradient(to bottom, var(--color-item) 10%, transparent);
}
.dragged-items-badge {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
background: var(--color-notification-badge);
color: var(--color-danger-text);
border-radius: 50%;
min-width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: var(--weight-bold);
padding: var(--space-0-5);
}
/* ── Swimlane group header ───────────────────────────────── */
/* Spans the full grid width in list view; no-op in grid view since
grid wraps it naturally. */
.resource-list__swimlane-header {
grid-column: 1 / -1;
padding: var(--space-1-5) var(--space-3) var(--space-1);
font-size: 0.72rem;
font-weight: var(--weight-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-faint);
border-bottom: 1px solid var(--color-border);
margin-top: var(--space-2);
cursor: default;
user-select: none;
}
.resource-list__swimlane-header:first-child,
.list-header + .resource-list__swimlane-header {
margin-top: 0;
}
/* Grouped-grid container (files route): a vertical stack of
(header + its own windowed card grid) per swimlane. Not a grid itself —
the card grid rides each VirtualList's inner window — so it just stacks. */
.files-grouped-grid {
display: flex;
flex-direction: column;
}
/* In that flex stack the `grid-column: 1 / -1` span is inert; the header
spans naturally as a block-level flex child. First header needs no top
margin (there is no list-header sibling before it in the grid path). */
.files-grouped-grid > .resource-list__swimlane-header--grid {
grid-column: auto;
}
.files-grouped-grid > .resource-list__swimlane-header--grid:first-child {
margin-top: 0;
}
2026-06-17 17:06:30 -06:00
/* When the header contains a rich DOM node (e.g. a user vignette for the
"owner" group-by), reset the typographic overrides that only make sense
for plain-text labels, and lay the node out inline. */
.resource-list__swimlane-header--node {
display: flex;
align-items: center;
padding: var(--space-1) var(--space-3);
text-transform: none;
letter-spacing: normal;
font-size: inherit;
font-weight: var(--weight-normal);
color: inherit;
}
/* ── Swimlane group card (list view only) ────────────────── */
/* When swimlane groups are present, dissolve the outer container into the
page background so each group card reads as its own panel. */
.files-list-view:has(.resource-list__swimlane-group) {
background-color: transparent;
box-shadow: none;
border-radius: 0;
overflow: visible;
gap: var(--space-2-5);
}
/* Each group is a self-contained card */
.files-list-view .resource-list__swimlane-group {
background-color: var(--color-item);
border-radius: var(--radius-xl);
box-shadow: 0 1px 3px var(--color-shadow-xs);
overflow: hidden;
}
/* The header inside a group card is always first — no extra top margin */
.files-list-view .resource-list__swimlane-group .resource-list__swimlane-header {
margin-top: 0;
}
/* ── Swimlane group wrapper (grid view) ──────────────────── */
/* The group wrapper must be transparent to the grid so .file-item children
continue to flow in the parent's columns (subgrid mirrors the column tracks).
The wrapper spans the full row width; items inside fill the columns naturally. */
.files-grid-view .resource-list__swimlane-group {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
gap: var(--space-5);
}
/* ── Section item modifiers ──────────────────────────────── */
/* — Favorites — */
.file-item.favorite-item {
border-left: 3px solid var(--color-warning-border);
[dir="rtl"] & {
border-right: 3px solid var(--color-warning-border);
border-left: unset;
}
}
/* list-view column override */
.file-item.favorite-item {
position: relative;
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
}
.file-item.favorite-item .favorite-indicator {
position: relative;
top: 0;
right: 0;
width: 30px;
height: 30px;
}
/* — Recent — */
.files-grid-view .file-item.recent-item {
border-left: 3px solid var(--color-recent-border);
[dir="rtl"] & {
border-right: 3px solid var(--color-recent-border);
border-left: unset;
}
}
/* list-view column override */
.file-item.recent-item {
position: relative;
grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px;
}
.file-item.recent-item .recent-indicator {
position: relative;
top: 0;
right: 0;
width: 30px;
height: 30px;
}
/* ── Path column (opt-in via ResourceListConfig.showPath) ──────────
* Visible in list view only — grid cards hide it because they have no
* dedicated column slot. itemTooltip still surfaces the path on hover.
*/
.files-list-view .file-item .path-cell {
color: var(--color-text-secondary);
font-size: var(--text-sm);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.files-grid-view .file-item .path-cell {
display: none;
}
/* ── Custom inline actions (ResourceListConfig.customActions) ──────
* Always visible in both list and grid view — used by trash for the
* restore / permanent-delete buttons that must be one click away.
*/
.btn-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--radius-lg);
border: none;
background: transparent;
cursor: pointer;
color: var(--color-text-subtle);
font-size: var(--text-md);
padding: 0;
}
.btn-action:hover {
background: var(--color-border-subtle);
color: var(--color-text-dark);
}
/* Opt-in modifier: hide the button until the row is hovered / focused.
Used by `/recent`'s per-row broom (a history-management action that
shouldn't distract from the row content at rest). Trash's Restore /
Delete stay on the plain `.btn-action` — those are the reason the
user opened trash, and hiding them would fail Fitts' law. */
.files-list-view .file-item .action-cell .btn-action--hover,
.files-grid-view .file-item .action-cell .btn-action--hover {
visibility: hidden;
transition: visibility var(--motion-fast) var(--ease-standard);
}
.files-list-view .file-item:hover .action-cell .btn-action--hover,
.files-list-view .file-item:focus-within .action-cell .btn-action--hover,
.files-grid-view .file-item:hover .action-cell .btn-action--hover,
.files-grid-view .file-item:focus-within .action-cell .btn-action--hover {
visibility: visible;
}
/* Legacy: a margin-top on `.btn-action` in grid view for the era when
these buttons flowed at the bottom of the card. Kept for any
free-standing use outside the corner cluster; reset inside
`.action-cell` (line ~745) so the broom / restore / delete pills
align with the kebab and star. */
2026-06-17 17:06:30 -06:00
.files-grid-view .file-item .btn-action {
margin-top: var(--space-1);
}