Files
siftlode/frontend/src/modules/plex/PosterTile.tsx
T
peter 63a0fecc8b refactor(frontend): the flat components/ folder moves into shell/, modules/<page>/ and features/<name>/
A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform
layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each
page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in
features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/
(ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows).

Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the
contract tests that scan sources now scan the whole app source, not only components/ — each glob
was counted before and after so none went green over fewer files. The locale-format rule matched
`/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton
check; it now covers every folder that holds components, with a test per folder.
2026-10-22 23:12:45 +02:00

74 lines
3.3 KiB
TypeScript

import { type ReactNode } from "react";
import { hueOf, hueTileGradient } from "../../lib/hueTile";
import { navItemAttrs } from "../../lib/listNav";
import { MEDIA_IMG } from "../../components/ui/mediaCardStyles";
import { Pressable } from "../../components/ui/Pressable";
import { cardImage } from "../../lib/plexImage";
import RemoteImage from "../../components/ui/RemoteImage";
// A browse tile split in two: the TOP half shows up to 2 montage posters (from /api/plex/image/{key}),
// the BOTTOM half is a solid colour-hash band that carries the label — so the text is always readable
// (a full-bleed poster collage drowned it). A group with no art falls back to an all-colour tile, never
// blank. Shared by the Discover genre tiles and the Playlist/Collection browse tiles.
//
// Hover = the media card's: the shadow lifts and the art zooms inside its half, plus the
// accent edge glass-hover gives a pressable panel. It had `glass-hover` on a tile with no border, so
// the only visible change was a shadow under a colour band — the owner read it as no hover at all.
export default function PosterTile({
title,
subtitle,
topRight,
art,
onClick,
className,
navIndex,
}: {
title: string;
subtitle?: ReactNode; // a bottom sub-line under the title (e.g. "12 items · 4h 30m")
topRight?: ReactNode; // top-right corner content on the colour band (e.g. an icon)
art?: string[]; // up to 2 poster rating_keys; fewer/none falls back to the colour hash
onClick: () => void;
className?: string; // caller sets the height (e.g. "h-20" / "h-28")
navIndex?: number; // position in a keyboard-walkable grid (lib/listNav); Enter is the button's own
}) {
const posters = (art ?? []).slice(0, 2);
const band = hueTileGradient(hueOf(title));
return (
<Pressable
selectable
onClick={onClick}
title={title}
{...(navIndex !== undefined && navItemAttrs(navIndex))}
className={`group relative flex flex-col overflow-hidden rounded-xl border border-border shadow-md transition hover:border-accent/55 hover:shadow-2xl ${className ?? ""}`}
>
{/* Top half: 2 montage posters, or the colour hash when a group has no art. */}
<div
className="grid h-1/2 min-h-0 grid-cols-2 overflow-hidden"
aria-hidden
style={posters.length === 0 ? { background: band } : undefined}
>
{posters.length > 0 &&
[0, 1].map((i) =>
posters[i] ? (
<RemoteImage
key={i}
{...cardImage(`/api/plex/image/${posters[i]}`)}
alt=""
loading="lazy"
className={MEDIA_IMG}
/>
) : (
<div key={i} style={{ background: hueTileGradient(hueOf(title + i)) }} />
),
)}
</div>
{/* Bottom half: solid colour band — the label lives here so it stays readable over any art. */}
<div className="relative flex flex-1 flex-col justify-end p-2.5" style={{ background: band }}>
{topRight != null && <span className="absolute right-2 top-2">{topRight}</span>}
<div className="truncate text-sm font-semibold text-white drop-shadow">{title}</div>
{subtitle != null && <div className="truncate text-2xs text-white/80">{subtitle}</div>}
</div>
</Pressable>
);
}