feat(ui): E31 S4c — the channel header folds, the Plex pages fill their hero, the player gets a side panel
- Channel page: the banner and identity block fold away once the grid scrolls (ui/scrollFold: folds past 32px only if the page can stay scrolled, unfolds at the very top) and the name rides in the tab row. One Back button in both states keeps its focus; folded at a tablet width the actions wrap instead of pushing the page sideways. - Plex movie and show pages: the TMDB facts over the key crew are the hero's second column (plexDetailAside), the keywords sit under the text; the show page shows them for the first time, with the same three toggles. A collection's hero is sized to its text. - YouTube player, 1280px and wider: the title, channel, actions, description and queue sit in a side panel beside the video, which takes the window's height (1438x809 at 1920x911, was ~1008 wide). The wheel scrolls that panel while it can and turns the volume elsewhere. - DESIGN.md decision log: the keywords' place under the text.
This commit is contained in:
@@ -174,8 +174,11 @@ describe("PlexInfo enriched detail (page)", () => {
|
||||
|
||||
// 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]";
|
||||
// E31 (the owner's pick "A"): the hero's text stopped at ~55% of the panel. The facts and the key crew are
|
||||
// its second column now (a grid that splits in a wide hero), the keywords sit under the text, and under
|
||||
// the hero only the collection strips and the cast remain, stacked.
|
||||
describe("PlexInfo — the hero's second column (page)", () => {
|
||||
const TWO_COLUMNS = "@5xl:grid-cols-[minmax(0,1fr)_minmax(0,24rem)]";
|
||||
// The strips are Carousels, which watch their size.
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
@@ -186,7 +189,8 @@ describe("PlexInfo layout B (page)", () => {
|
||||
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 follows = (a: HTMLElement, b: HTMLElement) =>
|
||||
!!(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
const full: PlexItemDetail = {
|
||||
...detail,
|
||||
cast: [{ name: "An Actor", role: "Hero" }],
|
||||
@@ -200,38 +204,51 @@ describe("PlexInfo layout B (page)", () => {
|
||||
],
|
||||
};
|
||||
|
||||
it("stacks the collection, the crew and the cast in the main column, the facts beside it", () => {
|
||||
it("puts the facts over the crew beside the text, the keywords under it, the rest below the hero", () => {
|
||||
wrap(<PlexInfo onPlayItem={noop} detail={full} variant="page" />);
|
||||
const aside = screen.getByTestId("detail-aside");
|
||||
expect(aside.contains(heading("plex.info.facts.title"))).toBe(true);
|
||||
expect(aside.contains(heading("plex.info.crew"))).toBe(true);
|
||||
expect(follows(heading("plex.info.facts.title"), heading("plex.info.crew"))).toBe(true);
|
||||
// The grid's two children: the poster + text, and the column.
|
||||
const grid = aside.parentElement!;
|
||||
expect(grid.className).toContain(TWO_COLUMNS);
|
||||
const main = grid.children[0]!;
|
||||
const side = grid.children[1];
|
||||
expect(side).toBe(aside);
|
||||
expect(main.contains(screen.getByText("A summary."))).toBe(true);
|
||||
expect(main.contains(screen.getByTestId("detail-keywords"))).toBe(true);
|
||||
// Below the hero (the panel under the title): the collection, then the cast — and neither in it.
|
||||
const hero = grid.parentElement!;
|
||||
expect(hero.contains(heading("Test Movie"))).toBe(true);
|
||||
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]);
|
||||
expect(hero.contains(coll) || hero.contains(cast)).toBe(false);
|
||||
expect(follows(hero, coll) && follows(coll, cast)).toBe(true);
|
||||
});
|
||||
|
||||
it("with nothing for the main column, the sidebar's sections take the full width", () => {
|
||||
wrap(
|
||||
<PlexInfo
|
||||
onPlayItem={noop}
|
||||
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]);
|
||||
it("without facts or crew there is no second column; the keywords still sit under the text", () => {
|
||||
const bare: PlexItemDetail = {
|
||||
...detail,
|
||||
year: null,
|
||||
duration_seconds: null,
|
||||
content_rating: null,
|
||||
studio: null,
|
||||
tmdb: {
|
||||
...detail.tmdb!,
|
||||
budget: null,
|
||||
revenue: null,
|
||||
studios: [],
|
||||
production_countries: [],
|
||||
crew: [],
|
||||
},
|
||||
};
|
||||
wrap(<PlexInfo onPlayItem={noop} detail={bare} variant="page" />);
|
||||
expect(screen.queryByTestId("detail-aside")).toBeNull();
|
||||
expect(screen.queryByRole("heading", { name: "plex.info.facts.title" })).toBeNull();
|
||||
const kw = screen.getByTestId("detail-keywords");
|
||||
expect(document.querySelector(`[class*="${TWO_COLUMNS}"]`)).toBeNull();
|
||||
expect(follows(heading("Test Movie"), kw)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user