feat(design): the three 375 rows (E28 S3f3)
- Download row: answers to its own width (container query); below 40rem
it stacks: picture beside a two-line title, the meta line across the
row (one line, never wraps: zoomed on the iPad its pieces broke apart),
then the source link and the actions on one wrapping line. The paste
field takes a line of its own when the buttons would squeeze it.
- YT playlist row: below 42rem the length rides on the picture and the
title runs two lines over the channel.
- Channels tag row: the suggestions take a line of their own below
40rem (the chip lane was 0 px and the row's menu off the screen).
- Pager: on a phone the arrows drop their words (names kept), and the
size label stays one line.
- Lint: a class split where the interpolation is the variant itself
(${NARROW}:w-24) is refused too; it had shipped with no CSS.
This commit is contained in:
@@ -155,3 +155,52 @@ describe("DownloadRow · keyboard", () => {
|
||||
expect(container.innerHTML).not.toContain("[&_button]");
|
||||
});
|
||||
});
|
||||
|
||||
// E28 S3f3: the row answers to its own width (a container query) and its meta line never wraps — on
|
||||
// the iPad, zoomed in, its "18.9 / MB" pieces broke into a jumble (the owner, 2026-09-27). jsdom lays
|
||||
// nothing out, so this holds the structure the layout rests on; the widths were measured in a browser.
|
||||
describe("DownloadRow · narrow", () => {
|
||||
it("keeps the meta line one cut line of inline text, not a flex row that wraps its pieces", () => {
|
||||
const { container } = render(
|
||||
<DownloadRow job={{ ...withAsset("mp4"), created_at: "2026-09-20T10:00:00Z" }} />,
|
||||
);
|
||||
const meta = container.querySelector("[data-dl-meta]")!;
|
||||
expect(meta.className.split(/\s+/)).toContain("truncate");
|
||||
expect(meta.className).not.toMatch(/(^|\s)(inline-)?flex(\s|$)/);
|
||||
for (const el of meta.querySelectorAll("*"))
|
||||
expect(el.className.toString()).not.toMatch(/(^|\s)(inline-)?flex(\s|$)/);
|
||||
});
|
||||
|
||||
it("is its own size container and stacks below 40rem", () => {
|
||||
const { container } = render(<DownloadRow job={withAsset("mp4")} />);
|
||||
const row = container.firstElementChild as HTMLElement;
|
||||
expect(row.className).toContain("@container/dlrow");
|
||||
const grid = row.firstElementChild as HTMLElement;
|
||||
expect(grid.className).toContain(
|
||||
"@max-[40rem]/dlrow:[grid-template-areas:'thumb_head''meta_meta''foot_foot''prog_prog']",
|
||||
);
|
||||
});
|
||||
|
||||
it("stacked, puts the source link and the actions on ONE wrapping line, not a line each", () => {
|
||||
const { container } = render(
|
||||
<DownloadRow job={{ ...withAsset("mp4"), source_url: "https://example.com/v" }}>
|
||||
<IconBtn onClick={() => {}} title="play">
|
||||
x
|
||||
</IconBtn>
|
||||
</DownloadRow>,
|
||||
);
|
||||
const link = container.querySelector("a[href='https://example.com/v']")!;
|
||||
const foot = screen.getByTitle("play").closest(".contents")!;
|
||||
expect(foot.contains(link)).toBe(true);
|
||||
for (const c of ["@max-[40rem]/dlrow:[grid-area:foot]", "@max-[40rem]/dlrow:flex-wrap"])
|
||||
expect(foot.className).toContain(c);
|
||||
});
|
||||
|
||||
it("draws nothing for an empty link list (a bare 0 once leaked into the row)", () => {
|
||||
const { container } = render(<DownloadRow job={{ ...withAsset("mp4"), extra_links: [] }} />);
|
||||
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
||||
const texts: string[] = [];
|
||||
while (walker.nextNode()) texts.push(walker.currentNode.textContent ?? "");
|
||||
expect(texts).not.toContain("0");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user