Web frontend
This commit is contained in:
90
web/src/components/Cover.tsx
Normal file
90
web/src/components/Cover.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/** 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 { coverUrl } from "../api/client";
|
||||
import type { Album } from "../api/types";
|
||||
import { aspectOf, coverBackground, 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?: React.CSSProperties;
|
||||
/** Print the title over generated art. Off for thumbnails, where it would not fit. */
|
||||
label?: boolean;
|
||||
}
|
||||
|
||||
export function Cover({ album, size, fit = "width", radius, className, style, label }: Props) {
|
||||
const book = isBook(album);
|
||||
const defaultRadius = book ? "6px 18px 18px 6px" : "16px";
|
||||
const box: React.CSSProperties =
|
||||
fit === "height"
|
||||
? { height: "100%", width: "auto", aspectRatio: aspectOf(album) }
|
||||
: { width: "100%", aspectRatio: aspectOf(album) };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
position: "relative",
|
||||
flex: "none",
|
||||
borderRadius: radius ?? defaultRadius,
|
||||
overflow: "hidden",
|
||||
background: coverBackground(album, Math.max(6, Math.round(size / 13))),
|
||||
...box,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{!album.has_cover && label && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: book ? "10% 14% 10% 20%" : "10%",
|
||||
textAlign: "center",
|
||||
fontSize: Math.max(11, Math.round(size / 11)),
|
||||
fontWeight: 900,
|
||||
lineHeight: 1.15,
|
||||
color: "oklch(99% 0 0 / .92)",
|
||||
textShadow: "0 2px 8px oklch(15% 0.05 210 / .6)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{album.title}
|
||||
</div>
|
||||
)}
|
||||
{album.has_cover && (
|
||||
<img
|
||||
src={coverUrl(album.id)}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
// Books keep a sliver of spine showing on the right, so the shelf metaphor
|
||||
// survives contact with real square artwork.
|
||||
clipPath: book ? "inset(0 5% 0 0)" : undefined,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user