feat(design): one media card, and Home's hero and shelves as cards (E28 S3h)
- ui/MediaCard + mediaCardStyles: one frame for every video, Plex title and Home tile; the meta line pinned to the bottom edge, the length a badge on the picture, the media Play only where the click plays, the hover row at the picture's bottom-right (the owner's pick C). - Shelf density: on any strip a one-line title over one meta line; mixed Home shelves are landscape; rails turn content-visibility off (a card's reserved height had become the strip's). Home slot 232px, so the Play and the hover row no longer touch; a landscape shelf takes the small Play. - Home hero is a card (hover row, the whole menu, a touch "…"), three quarters wide and centred; channel / playlist tiles get Play, open and "…"; a show tile acts on its next episode, ⓘ opens the show. - Plex preview: a ~0.9 s mouse rest opens a passive panel with the whole title, facts and synopsis (popover `pointerOnly`); the synopsis and title tooltips are gone. - Compact list row B: the title leads, "…" at the end, toggles over the date on hover. - Media-info layout B (collections, crew, cast beside the facts sidebar; an empty column is not drawn); filtering values are chips there and on the show page; "Cast" over the cast-only strips. - Browse tiles hover like a card; prose capped by the `measure` utility.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
// countries), the keyword chips (→ onFilter), and the full crew (→ onOpenPerson). i18n is
|
||||
// key-passthrough; PlexInfo is prop-driven (no queries) so it just needs a QueryClient for its cache
|
||||
// reads. The overlay variant is asserted to stay lean (no facts/keywords/crew).
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { describe, it, expect, afterEach, beforeAll, vi } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
@@ -156,3 +156,60 @@ describe("PlexInfo enriched detail (page)", () => {
|
||||
expect(screen.getByText("IMDb")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// E28 S3h, the owner's layout B: a main column (collection strips → crew → cast) beside a sidebar of
|
||||
// facts (details, keywords); a column with nothing in it is not rendered and the other takes the width.
|
||||
describe("PlexInfo layout B (page)", () => {
|
||||
const TWO_COLUMNS = "lg:grid-cols-[minmax(0,1fr)_20rem]";
|
||||
// The strips are Carousels, which watch their size.
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
||||
});
|
||||
const heading = (name: string) => screen.getByRole("heading", { name });
|
||||
const column = (el: HTMLElement) => el.closest("div.flex-col") as HTMLElement;
|
||||
const full: PlexItemDetail = {
|
||||
...detail,
|
||||
cast: [{ name: "An Actor", role: "Hero" }],
|
||||
collections: [
|
||||
{
|
||||
id: "c1",
|
||||
title: "The Collection",
|
||||
source: "plex",
|
||||
items: [{ id: "m2", type: "movie", title: "Sequel", thumb: "/api/plex/image/m2" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("stacks the collection, the crew and the cast in the main column, the facts beside it", () => {
|
||||
wrap(<PlexInfo detail={full} variant="page" />);
|
||||
const coll = heading("The Collection");
|
||||
const crew = heading("plex.info.crew");
|
||||
const cast = heading("plex.info.cast");
|
||||
const facts = heading("plex.info.facts.title");
|
||||
const follows = (a: HTMLElement, b: HTMLElement) =>
|
||||
!!(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(follows(coll, crew) && follows(crew, cast)).toBe(true);
|
||||
const main = column(coll);
|
||||
expect(main.contains(crew) && main.contains(cast)).toBe(true);
|
||||
const side = column(facts);
|
||||
expect(side).not.toBe(main);
|
||||
expect(side).toBe(column(heading("plex.info.keywords")));
|
||||
// the two columns are the two children of the one grid
|
||||
expect(main.parentElement!.className).toContain(TWO_COLUMNS);
|
||||
expect([...main.parentElement!.children]).toEqual([main, side]);
|
||||
});
|
||||
|
||||
it("with nothing for the main column, the sidebar's sections take the full width", () => {
|
||||
wrap(<PlexInfo detail={{ ...detail, tmdb: { ...detail.tmdb!, crew: [] } }} variant="page" />);
|
||||
expect(screen.queryByRole("heading", { name: "plex.info.crew" })).toBeNull();
|
||||
const side = column(heading("plex.info.facts.title"));
|
||||
expect(side).toBe(column(heading("plex.info.keywords")));
|
||||
expect(side.parentElement!.className).not.toContain(TWO_COLUMNS);
|
||||
expect([...side.parentElement!.children]).toEqual([side]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user