/** Album art, or a generated stand-in built from the album's own three colours. * * The gradient sits underneath the image rather than instead of it, so a cover that is * still loading - or an album that has none - never shows a hole. * * The shape comes from the media type and from nowhere else: square for an album, * taller than wide for an audiobook. Callers pick which axis is fixed, never the ratio. */ import type { CSSProperties } from "react"; import { THUMB_MAX_SIZE, coverUrl } from "../api/client"; import type { Album } from "../api/types"; import { aspectOf, coverBackground, coverUnderlay, isBook } from "../lib/covers"; interface Props { album: Album; /** Rendered size in px along the fixed axis; drives the stripe width so the generated * pattern scales with the art. */ size: number; /** Which axis the parent constrains. `height` keeps rows and grids even when a book * and an album sit side by side. */ fit?: "width" | "height"; radius?: string; className?: string; style?: CSSProperties; /** Print the title over generated art. Off for thumbnails, where it would not fit. */ label?: boolean; /** A reward-gated album/track not yet earned - shows a placeholder cover instead of * the real art or title, regardless of `has_cover`. */ locked?: boolean; } export function Cover({ album, size, fit = "width", radius, className, style, label, locked }: Props) { const book = isBook(album); const defaultRadius = book ? "6px 18px 18px 6px" : "16px"; const box: CSSProperties = fit === "height" ? { height: "100%", width: "auto", aspectRatio: aspectOf(album) } : { width: "100%", aspectRatio: aspectOf(album) }; return (
{locked ? ( ) : ( <> {!album.has_cover && label && (
{album.title}
)} {album.has_cover && ( )} )}
); }