fix(files): a steady list header, every user on the Storage tab, the name gets the row's room

- The Files list starts at the scroller's top: the padding above it let the sticky header ride up
  12px on the first scroll, then stop.
- Users -> Storage lists every user with their quota, not only those already holding a file — a
  quota is set before anyone downloads.
- On a wide Files list only the "…" holds a place; Copy link, Share and Save come up over the date
  while the row is hovered or holds the keyboard (a card keeps them in its line). Keeps is narrower.
This commit is contained in:
2026-10-11 20:33:48 +02:00
parent 078194731b
commit 6707550513
5 changed files with 221 additions and 171 deletions
@@ -1,7 +1,8 @@
// @vitest-environment jsdom
//
// The Files page: a row keeps Copy link, Share, Save and its "…" in sight — a slot an action does not
// apply to stays empty, so the icons line up down the list — and its every action is its ONE menu,
// The Files page: a row has Copy link, Share and Save (on a wide list, shown while it is hovered or
// focused) and its "…" — a slot an action does not apply to stays empty, so the icons line up down
// the list — and its every action is its ONE menu,
// opened by the "…" or a right-click. Renaming happens in the row; moving goes to an own folder or
// back home; a shared file is the owner's, so its menu only plays, saves, cuts or drops it from my
// list. i18n is key-passthrough, so labels read as keys.
@@ -177,6 +178,20 @@ describe("Files · my file's row", () => {
]);
});
it("holds back the quick icons on a wide row until it is hovered or focused; the '…' stays", () => {
mocks.files = [file(2)];
mocks.folders = [YT];
mount();
const row = rowOf("Clip 2");
const quick = row.querySelector<HTMLElement>("[data-quick-actions]")!;
expect(row.className).toMatch(/(^|\s)group(\s|$)/);
expect(quick.className).toContain("md:opacity-0");
expect(quick.className).toContain("md:group-hover:opacity-100");
expect(quick.className).toContain("md:group-focus-within:opacity-100");
expect(quick.contains(row.querySelector('[aria-label="files.actions.share"]'))).toBe(true);
expect(quick.contains(moreOf("Clip 2"))).toBe(false);
});
it("keeps the slot of an action a plain file lacks, so the icons line up", () => {
mocks.files = [file(3, { job_kind: "upload", asset: { media_kind: "file" } } as never)];
mocks.folders = [YT];
@@ -388,6 +403,14 @@ describe("Files · a shared file's row", () => {
});
describe("Files · the view", () => {
it("starts the list at the scroller's top, so its sticky header does not jump on the first scroll", () => {
mocks.files = [file(2)];
mocks.folders = [YT];
mount();
const list = screen.getByRole("table").closest<HTMLElement>(".max-w-7xl")!;
expect(list.className).not.toMatch(/(^|\s)(pt|py|p)-/);
});
it("reads one folder's files and names the folder", () => {
mocks.files = [file(2, { folder_id: 2 })];
mocks.folders = [YT, OWN];