1448 lines
51 KiB
CSS
1448 lines
51 KiB
CSS
/* ============================================================
|
||
* 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);
|
||
}
|