From a31beee46b7bfebc4dedc77e6841219e336c2787 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 10 Oct 2026 14:59:07 +0200 Subject: [PATCH] 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. --- client_v2/src/routes/dashboard/+page.svelte | 14 ++++++- tests_frontend_v2/tests/dashboard.spec.ts | 44 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests_frontend_v2/tests/dashboard.spec.ts diff --git a/client_v2/src/routes/dashboard/+page.svelte b/client_v2/src/routes/dashboard/+page.svelte index 63483e1c..08d698c7 100644 --- a/client_v2/src/routes/dashboard/+page.svelte +++ b/client_v2/src/routes/dashboard/+page.svelte @@ -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. */ diff --git a/tests_frontend_v2/tests/dashboard.spec.ts b/tests_frontend_v2/tests/dashboard.spec.ts new file mode 100644 index 00000000..4a4f4718 --- /dev/null +++ b/tests_frontend_v2/tests/dashboard.spec.ts @@ -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}`); + } +});