Files
Oxicloud/frontend/src/lib/styles/ported/resourceList.css
T
2026-07-27 01:33:09 +02:00

1448 lines
51 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
* 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);
}
/* ── 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"] {
/* Bumped from 17px to 20px so the row-selection checkbox reads at
a similar visual weight to the 28x28 action buttons that sit at
the other end of the row (Ed's 2026-07-26 UX note). 20px is the
upper end of the browser-native checkbox range — beyond that
platforms start rendering an oversized-and-blurry glyph. */
width: 20px;
height: 20px;
cursor: pointer;
accent-color: var(--color-accent);
border-radius: var(--radius-sm);
}
/* Orange outline on hover for the row-selection checkbox (Ed's UX ask
2026-07-26). `outline` (not `border`) because native checkboxes in
list view honor `accent-color` but their border rendering is
platform-inconsistent — `outline` is drawn OUTSIDE the box and
doesn't reflow the layout. Grid view uses a custom-drawn checkbox
(see `.files-grid-view … input[type="checkbox"]` below) which
accepts real border styling; its hover override lives with that
block and beats this via specificity. Matches the "everything
hovers to accent" convention shared with row-action buttons. */
.list-header-checkbox input[type="checkbox"]:hover,
.file-item .checkbox-cell input[type="checkbox"]:hover {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.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;
}
/* 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,
.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). */
/* 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 {
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;
}
/* 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;
}
.files-list-view {
--files-list-columns: 36px minmax(200px, 2fr) 100px 110px 130px 200px;
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;
}
/* 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;
}
/* 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));
font-size: var(--text-md);
margin-bottom: 0;
flex-shrink: 0;
}
.list-header > .date-cell,
.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;
align-items: center;
gap: var(--space-1);
text-align: right;
}
/* 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;
}
/* 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);
/* Accent-orange hover — matches the grid view's `.file-actions:hover`
and the shared `.btn-action:hover` rule. Semantic overrides on
`.favorite-star:hover` / `.shared-button:hover` (gold / blue) take
precedence via more-specific selectors below. */
color: var(--color-accent);
}
/* List-view resting/active state colors for favorite (gold) and shared
(blue). Hover is intentionally NOT overridden here — the generic
`.action-cell button:not(.btn-action):hover → --color-accent` rule
above owns the orange hover for every button (kebab / favorite /
shared) per Ed's 2026-07-26 spec. This block only paints the
at-rest state on `.active` rows so the star / shared chip stays
discoverable on a quiet row. */
.files-list-view .file-item .action-cell button.favorite-star.active {
color: var(--color-star-text-hover);
}
.files-list-view .file-item .action-cell button.shared-button.active {
color: var(--color-badge-blue-text);
}
/* Fav-star + shared-button — hidden on quiet rows, visible on row hover,
and always visible when their `.active` class is set (so a favorited /
shared row is discoverable without mousing over).
`opacity: 0` + `pointer-events: none` (instead of `visibility: hidden`)
so the hide transitions match the kebab's fade — Ed's 2026-07-26 UX
note: pre-refactor kebab faded over motion-fast while star/shared
snapped out instantly (visibility flips have no animatable value),
producing a staggered exit when the pointer left a row. The slot
still reserves layout because the button geometry is unchanged; only
its paint is toggled. */
.files-list-view .file-item .action-cell button.favorite-star,
.files-list-view .file-item .action-cell button.shared-button {
opacity: 0;
pointer-events: none;
border: none;
transition: opacity var(--motion-fast) var(--ease-standard);
}
/* Strict reveal: star + shared appear ONLY on `.active` (row is
favorited / shared) or pointer hover — Ed's 2026-07-26 spec. No
`:focus-within` reveal, so keyboard focus on the row-body (name /
path cells) does NOT flash the actions cluster. Quiet rows stay
quiet at rest. */
.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 {
opacity: 1;
pointer-events: auto;
}
/* Reveal the kebab on hover for cleaner rows — but only on hover-capable
devices, so touch users (no hover) keep it always tappable. Applies
to both list and grid views because both keep the kebab inside
`.action-cell`.
Keyboard accessibility comes from `:focus-visible` on the kebab
button itself (below), NOT `:focus-within` on the row. Using
`:focus-within` on the row was a lingering-visibility trap:
• dragstart landed focus on the dragged descendant → row
`:focus-within` stayed true after the pointer left → kebab
stayed visible on an otherwise-idle row.
• Opening a context-menu / ShareDialog portal moved focus outside
the row (good) but if focus briefly bounced through the kebab
first, the reveal could persist through the transition.
Ed's 2026-07-26 report: "when starting dragging or when using the
share dialog, I have the [...] button that remains visible." */
@media (hover: hover) {
.files-list-view .file-item .action-cell button.file-actions,
.files-grid-view .file-item .action-cell button.file-actions {
opacity: 0;
transition: opacity var(--motion-fast) var(--ease-standard);
}
.files-list-view .file-item:hover .action-cell button.file-actions,
.files-grid-view .file-item:hover .action-cell button.file-actions,
.files-list-view .file-item .action-cell button.file-actions:focus-visible,
.files-grid-view .file-item .action-cell button.file-actions:focus-visible {
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);
/* 30x30 matches the action-cell chip pills (`.file-actions`, star,
shared, `.btn-action`) so the checkbox and the corner-cluster
buttons read as siblings of one visual size (Ed 2026-07-26). */
width: 30px;
height: 30px;
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;
/* Custom-drawn glyph sized proportionally to the 30x30 chip pill —
bumped from 18x18 to match the list-view checkbox's 20x20. */
width: 20px;
height: 20px;
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);
}
/* Grid view uses a custom-drawn checkbox (`appearance: none`) so we can
swap the real border color on hover — cleaner than the list-view
`outline` trick, and no double-ring on this variant. Specificity
(0,4,1 + 0,0,1) beats the shared `input[type="checkbox"]:hover`
outline rule above so the grid-view chip doesn't get both a border
AND an outline. */
.files-grid-view .file-item .checkbox-cell input[type="checkbox"]:hover {
border-color: var(--color-accent);
outline: none;
}
.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 (30px chip + inline gap)
so the left group aligns visually with the checkbox row. Kept in
sync with `.checkbox-cell` width above — the two share this
constant. */
left: calc(var(--space-3) + 8px + 30px + var(--space-2));
right: calc(var(--space-3) + 8px);
display: flex;
align-items: center;
justify-content: flex-start;
gap: var(--space-1);
}
.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).
`opacity + pointer-events` so the fade timing matches the kebab and
every `.btn-action--hover` — see the list-view block above for why. */
.files-grid-view .file-item .action-cell .favorite-star,
.files-grid-view .file-item .action-cell .shared-button {
opacity: 0;
pointer-events: none;
transition: opacity var(--motion-fast) var(--ease-standard);
}
/* Strict reveal: same rule as the list view — `.active` OR pointer
hover, no `:focus-within` (Ed's 2026-07-26 spec). */
.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 .action-cell .favorite-star.active,
.files-grid-view .file-item .action-cell .shared-button.active {
opacity: 1;
pointer-events: auto;
}
/* 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.
NOTE: no `opacity: 1` here. Per-button reveal rules above own the
opacity (star/shared: `.active` + `:hover`; kebab + btn-action:
`:hover`). Pre-refactor this block force-set `opacity: 1` on the
assumption that reveal lived on the parent `.action-cell` — that
assumption is gone (Ed 2026-07-26), and the leftover made star +
shared appear always-visible in grid view regardless of hover /
`.active` state. */
.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;
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;
border: none;
border-radius: var(--radius-full);
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;
align-items: center;
justify-content: center;
color: var(--color-text);
font-size: var(--text-md);
cursor: pointer;
}
/* Unified row-action hover: every button in the grid-view corner cluster
(kebab, star, shared, btn-action*) turns accent-orange on hover.
Semantic states are conveyed by the `.active` class, not by hover
color, so favorite = gold when starred, shared = blue when shared,
both regardless of pointer position (see `.active` rules below).
Ed's 2026-07-26 UX call: "orange for mouse over on all buttons;
blue only for active shared."
Selector specificity (0,4,1 + 0,0,1 = high) beats the chip-visual
base rule at ~line 852 (`.files-grid-view .file-item .action-cell
.btn-action { color: var(--color-text) }`, 0,4,0), which is why the
simpler `.btn-action:hover` didn't take effect inside the corner
cluster. */
.files-grid-view .file-item .action-cell .file-actions:hover,
.files-grid-view .file-item .action-cell .btn-action:hover,
.files-grid-view .file-item .action-cell .favorite-star:hover,
.files-grid-view .file-item .action-cell .shared-button:hover {
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 — resting/active state colors only.
HOVER color for both lives in the unified `.action-cell button:hover
→ --color-accent` rule above; the per-state palette here only fires
when the button is NOT hovered. Convention (Ed 2026-07-26):
• hover → orange (accent) — every row action
• star.active (not hover) → gold
• shared.active (not hover) → blue
`.active` retains its color even on hover for `favorite-star` (star
users expect gold-on-gold on the currently-starred item; losing the
glyph mid-click reads as broken) but yields to orange for `shared`
per Ed's explicit "blue only for active shared" — pointer-over on
an already-shared row should still communicate "you're about to
toggle something." */
.files-grid-view .file-item button.favorite-star,
.files-grid-view .file-item button.shared-button {
color: var(--color-text-subtle);
font-size: 15px;
line-height: var(--leading-none);
}
.files-grid-view .file-item button.favorite-star.active {
color: var(--color-star-text-hover);
}
.files-grid-view .file-item button.shared-button.active {
color: var(--color-badge-blue-text);
}
.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);
}
/* 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, `<UserAvatar>`'s `.ua` wrapper — and slapped 8 px of top/bottom
padding on them, which crushed the badge's `<img>` 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%;
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));
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;
}
/* 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);
/* Accent (orange) hover matches `.file-actions` (kebab) and the
favorite / shared button semantics — the pre-refactor grey-only
tint left `/recent`'s broom, `/trash`'s restore and `/search`'s
"open parent" reading as inert on hover next to the accented
kebab. Ed's 2026-07-26 UX ask: "all row buttons should change
color on hover, not just kebab and favorite." Section-specific
variants (`.btn-action--delete` in trash, `--on` in ShareDialog)
still win via more-specific selectors so red / etc. semantics
are preserved. */
color: var(--color-accent);
}
/* Opt-in modifier: hide the button until the row is hovered / focused.
Used by `/recent`'s per-row broom and `/search`'s open-parent
(history-management / navigational actions 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.
`opacity + pointer-events` (not `visibility`) so the fade timing
matches the kebab + star + shared button — Ed's 2026-07-26 UX note:
pre-refactor each reveal mechanism was different, producing a
staggered exit when the pointer left a row. */
.files-list-view .file-item .action-cell .btn-action--hover,
.files-grid-view .file-item .action-cell .btn-action--hover {
opacity: 0;
pointer-events: none;
transition: opacity var(--motion-fast) var(--ease-standard);
}
/* Reveal on hover OR when the button itself has keyboard focus. The
pre-fix `:focus-within` on the row was a lingering-visibility trap
during drag / dialog transitions — see the `.file-actions` block
above for the full rationale. `:focus-visible` on the button gives
keyboard users the same reveal without the row-scope side effect. */
.files-list-view .file-item:hover .action-cell .btn-action--hover,
.files-grid-view .file-item:hover .action-cell .btn-action--hover,
.files-list-view .file-item .action-cell .btn-action--hover:focus-visible,
.files-grid-view .file-item .action-cell .btn-action--hover:focus-visible {
opacity: 1;
pointer-events: auto;
}
/* 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. */
.files-grid-view .file-item .btn-action {
margin-top: var(--space-1);
}