Let dashboard cards use the screen height

Card bodies were capped at min(52vh, 420px), so a group of more than
about eight spools scrolled inside its card while most of a desktop
screen stayed empty below it.

The cap now reaches down to the footer for a single row of cards, but
stops at 1000px. A card only fetches its next page when its list is
scrolled, so a body taller than one page of chips (about 1500px) shows
no scrollbar and never loads the rest; the cap proposed in the issue,
calc(100dvh - 220px), did exactly that on a 2160px tall screen. Narrow
touch layouts keep the old cap so a card can't swallow the page's
scroll. A Playwright spec pins the paging on a tall screen.
This commit is contained in:
Donkie committed 2026-10-10 14:59:07 +02:00
1 parent 629b8228d8
commit a31beee46b
2 files changed
+56 -2

No files matched your search

+12 -2
View File
@@ -1012,8 +1012,13 @@
* scrolling it to the bottom fetches the next page. */
.card-body {
/* Single source of truth for the height cap: the reserved min-height (set inline from
* the spool count) is clamped to this too, so it never exceeds max-height. */
--card-body-cap: min(52vh, 420px);
* the spool count) is clamped to this too, so it never exceeds max-height.
*
* A single row of cards reaches down to the footer (#1242). The 1000px ceiling is
* not cosmetic: the next page only loads when the body is scrolled, so the body
* must stay shorter than one page of chips (PAGE × CHIP_H, about 1500px) or a tall
* screen would show the first page with no scrollbar and never fetch the rest. */
--card-body-cap: clamp(min(52vh, 420px), 100dvh - 240px, 1000px);
display: flex;
flex-direction: column;
gap: 6px;
@@ -1127,6 +1132,11 @@
.ctrl-label {
display: none;
}
/* Cards stack on a touch screen, and a body nearly as tall as the screen would
* swallow the swipes meant for scrolling the page past it. */
.card-body {
--card-body-cap: min(52vh, 420px);
}
}
/* Phone: keep the controls together on the first row (title · group-by · new group)
and give the hint the full width of the row below them. */
+44
View File
@@ -0,0 +1,44 @@
import { expect, test } from "./fixtures";
import { unique } from "./helpers";
/**
* A dashboard card shows its spools a page at a time and fetches the next page
* when its list is scrolled to the bottom. The card's height cap has to stay
* below one page of spools for that to work: on a tall screen a card that fits
* its whole first page has no scrollbar, so the rest would never load (#1242).
*/
const API_BASE =
process.env.SPOOLMAN_API_URL ?? `${process.env.SPOOLMAN_BASE_URL ?? "http://localhost:8001"}/api/v1`;
test.use({ viewport: { width: 1920, height: 2160 } });
test("a big group's card loads every spool on a tall screen", async ({ page, request }) => {
const location = unique("Rack");
const res = await request.post(`${API_BASE}/filament`, {
data: { name: unique("Filament"), density: 1.24, diameter: 1.75 },
});
expect(res.ok()).toBeTruthy();
const filamentId = (await res.json()).id;
// More than one page of spools (30).
const ids: number[] = [];
for (let i = 0; i < 40; i++) {
const spool = await request.post(`${API_BASE}/spool`, { data: { filament_id: filamentId, location } });
expect(spool.ok()).toBeTruthy();
ids.push((await spool.json()).id);
}
try {
await page.goto("/dashboard?by=location");
const card = page.locator(".card").filter({ has: page.getByText(location, { exact: true }) });
const body = card.locator(".card-body");
await expect(body.locator(".chip")).toHaveCount(30);
await body.evaluate((el) => el.scrollTo(0, el.scrollHeight));
await expect(body.locator(".chip")).toHaveCount(40);
} finally {
// Leave the shared database as the other specs expect it.
for (const id of ids) await request.delete(`${API_BASE}/spool/${id}`);
await request.delete(`${API_BASE}/filament/${filamentId}`);
}
});