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:
2026-09-27 00:54:19 +02:00
parent 6653d81a80
commit a453b77f04
30 changed files with 1882 additions and 769 deletions
+143
View File
@@ -0,0 +1,143 @@
import clsx from "clsx";
import { Bookmark, Check, ListPlus, MoreHorizontal, Star } from "lucide-react";
import { Reference, Sample } from "./labKit";
import { MediaCard, MediaPlayGlyph } from "../components/ui/MediaCard";
import {
MEDIA_BADGE,
MEDIA_BAR,
MEDIA_HOVER_LAYER,
MEDIA_PICTURE,
mediaPictureBox,
mediaTitleClass,
} from "../components/ui/mediaCardStyles";
import { iconButtonClass } from "../components/ui/buttonStyles";
import { HOVER_YIELD } from "../components/ui/reveal";
// E28 S3h — the one media card, DECIDED (owner, 2026-09-26/27: anatomy C, shelf density, the preview
// C, the Home hero A, list row B, info layout B). Drawn with the real ui/MediaCard and its classes;
// hover a card to see its Play and its hover row. The preview panel, the hero and the info page are
// live in the app (Plex, Home), not copied here.
const BAR_BUTTON = iconButtonClass({ variant: "media", size: "sm" });
function HoverRow({ save = false }: { save?: boolean }) {
return (
<div className={MEDIA_BAR}>
<span className="flex gap-1">
{[Check, ...(save ? [Bookmark] : []), ListPlus, MoreHorizontal].map((Icon, i) => (
<span key={i} className={BAR_BUTTON}>
<Icon />
</span>
))}
</span>
</div>
);
}
function Picture({
aspect,
length,
small = false,
save = false,
}: {
aspect: "poster" | "wide";
length: string;
small?: boolean;
save?: boolean;
}) {
return (
<div className={mediaPictureBox(aspect)}>
<div className={clsx(MEDIA_PICTURE, "bg-linear-to-br from-accent/45 to-card")}>
<span className={clsx(MEDIA_BADGE, "bottom-1.5 right-1.5", HOVER_YIELD)}>{length}</span>
<div className={MEDIA_HOVER_LAYER}>
<MediaPlayGlyph small={small} />
</div>
</div>
<HoverRow save={save} />
</div>
);
}
const score = (
<span className="inline-flex items-center gap-0.5">
<Star className="h-3 w-3 fill-current text-rating" aria-hidden />
7.8
</span>
);
const plexMeta = (
<div className="flex items-center gap-1.5 text-xs leading-6 text-muted">
<span>2003</span>
<span aria-hidden>·</span>
{score}
</div>
);
export function BatchD() {
return (
<div className="flex flex-col gap-4">
<Reference
id="s3h-card"
code="S3h"
title="The media card"
rule="One frame: the picture, the title, the meta pinned to the bottom edge. The length is a badge on the picture; the Play shows only where the click plays; the hover row docks bottom-right and the length steps aside. A grid card: two title lines and the full meta. On a shelf: one title line over one meta line, a 232px slot, and a landscape picture takes the smaller Play."
>
<div className="flex flex-wrap items-stretch gap-4">
<Sample label="Grid · a film (poster)">
<div className="grid w-44 grid-cols-1">
<MediaCard
dense
picture={<Picture aspect="poster" length="2h 34m" />}
title={
<div className={clsx("min-h-6", mediaTitleClass(true))}>
The Last Samurai, a title long enough to wrap
</div>
}
meta={plexMeta}
/>
</div>
</Sample>
<Sample label="Grid · a video">
<div className="grid w-72 grid-cols-1">
<MediaCard
picture={<Picture aspect="wide" length="18:04" save />}
title={<div className={mediaTitleClass(false)}>A video title</div>}
meta={
<>
<div className="text-sm text-muted">A channel</div>
<div className="mt-0.5 text-xs text-muted">12K views · 3 days ago</div>
</>
}
/>
</div>
</Sample>
<Sample label="Shelf · 232px, landscape, the smaller Play">
<div className="grid w-58 grid-cols-1">
<MediaCard
dense
picture={<Picture aspect="wide" length="2h 34m" small />}
title={
<div className={clsx("min-h-6", mediaTitleClass(true, true))}>
The Last Samurai, cut to one line on a shelf
</div>
}
meta={plexMeta}
/>
</div>
</Sample>
</div>
</Reference>
<Reference
id="s3h-measure"
code="S3h"
title="The reading measure"
rule="Prose (a synopsis, a biography, a channel's description) runs at most ~65 characters a line: the `measure` utility, wherever its container can be wider."
>
<p className="measure text-sm leading-relaxed text-muted">
Bourne is brought out of hiding once again by a reporter who is trying to unveil an
operation, an upgrade to a programme, in a series of newspaper columns. Information from
the reporter stirs a new set of memories, and Bourne must finally uncover his dark past.
</p>
</Reference>
</div>
);
}