The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
import { type ReactNode } from "react";
|
|
import { hueOf, hueTileGradient } from "../lib/hueTile";
|
|
import { navItemAttrs } from "../lib/listNav";
|
|
import { MEDIA_IMG } from "./ui/mediaCardStyles";
|
|
import { Pressable } from "./ui/Pressable";
|
|
import { cardImage } from "../lib/plexImage";
|
|
|
|
// 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] ? (
|
|
<img
|
|
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>
|
|
);
|
|
}
|