Web frontend

This commit is contained in:
2026-08-27 12:32:20 +02:00
parent d44c24ec97
commit edb6e5e027
97 changed files with 9535 additions and 195 deletions

View 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>
);
}