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:
@@ -65,7 +65,13 @@ vi.mock("../lib/api", () => ({
|
||||
discoverCardKey: (c: { source: string; id: string }) => `${c.source}:${c.id}`,
|
||||
}));
|
||||
|
||||
vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ setPage: vi.fn() }) }));
|
||||
const openChannel = vi.fn();
|
||||
vi.mock("./navigationContext", () => ({
|
||||
useNavigationActions: () => ({
|
||||
setPage: vi.fn(),
|
||||
openChannel: (...a: unknown[]) => openChannel(...a),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./plexContext", () => ({ usePlexActions: () => ({ setJumpTo: vi.fn() }) }));
|
||||
|
||||
import Home from "./Home";
|
||||
@@ -150,6 +156,7 @@ const item = (label: string) => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
openChannel.mockReset();
|
||||
// Home keeps its question on the history entry (Back restores it): each test starts a fresh one.
|
||||
window.history.replaceState(null, "");
|
||||
discover.mockReset().mockResolvedValue(payload());
|
||||
@@ -166,6 +173,8 @@ describe("Home — the comfort shelf's context menu", () => {
|
||||
fireEvent.contextMenu(await screen.findByText("Comfort B"));
|
||||
expect(labels()).toEqual([
|
||||
"home.hero.play",
|
||||
// A channel tile opens its channel (the owner, 2026-09-27: it had nothing a channel offers).
|
||||
"card.menu.openChannel",
|
||||
"home.shelf.moveFirst",
|
||||
"home.shelf.moveLeft",
|
||||
"home.shelf.moveRight",
|
||||
@@ -341,3 +350,28 @@ describe("Home — the comfort shelf's context menu", () => {
|
||||
expect(item("home.pin.add").disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// The owner, 2026-09-27: every Home card offers what the same card offers elsewhere.
|
||||
describe("Home — a card's own actions", () => {
|
||||
it("opens a pinned channel from its tile's ↗", async () => {
|
||||
mount();
|
||||
await screen.findByText("Comfort B");
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: "card.menu.openChannel" })[1] as HTMLElement,
|
||||
);
|
||||
expect(openChannel).toHaveBeenCalledWith("UCb", "Comfort B");
|
||||
});
|
||||
|
||||
it("gives the hero the card's whole menu, not the pin row alone", async () => {
|
||||
discover.mockResolvedValue({
|
||||
...payload(),
|
||||
hero: { tier: "surprise", card: base("h1", "Hero video") },
|
||||
});
|
||||
mount();
|
||||
fireEvent.contextMenu(await screen.findByRole("heading", { name: "Hero video" }));
|
||||
const rows = labels();
|
||||
expect(rows[0]).toBe("home.hero.play");
|
||||
expect(rows).toContain("card.markWatched");
|
||||
expect(rows).toContain("card.menu.openChannel");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user