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,189 @@
/** The album detail sheet: cover, metadata, and a numbered track list. */
import type { Album } from "../api/types";
import { isBook, unitLabel } from "../lib/covers";
import { clock } from "../lib/format";
import { Cover } from "./Cover";
interface Props {
album: Album;
currentAlbumId: string | null;
currentTrackIndex: number;
onClose: () => void;
onPlay: (trackIndex: number) => void;
}
export function AlbumModal({
album,
currentAlbumId,
currentTrackIndex,
onClose,
onPlay,
}: Props) {
const book = isBook(album);
return (
<div className="overlay" style={{ zIndex: 4, background: "oklch(15% 0.03 210 / .6)" }} onClick={onClose}>
<div
onClick={(event) => event.stopPropagation()}
style={{
background: "oklch(96% 0.012 210)",
borderRadius: 26,
padding: 26,
width: 640,
maxWidth: "100%",
maxHeight: "100%",
overflow: "auto",
boxShadow: "0 24px 60px oklch(10% 0.04 210 / .55)",
}}
>
<div style={{ display: "flex", gap: 20, alignItems: "flex-start" }}>
<div style={{ width: 150, flex: "none" }}>
<Cover album={album} size={150} radius={book ? "10px 18px 18px 10px" : "16px"} label />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 26,
fontWeight: 900,
color: "oklch(22% 0.03 210)",
lineHeight: 1.15,
}}
>
{album.title}
</div>
<div
style={{
fontSize: 15,
fontWeight: 700,
color: "oklch(45% 0.03 210 / .85)",
marginTop: 2,
}}
>
{album.artist}
</div>
<div
style={{
fontSize: 13,
fontWeight: 700,
color: "oklch(50% 0.03 210 / .7)",
marginTop: 2,
}}
>
{book ? "📖 Hörbuch" : "🎵 Musik"} · {unitLabel(album, album.tracks.length)} ·{" "}
{clock(album.duration)}
{album.figure && ` · 🧸 ${album.figure}`}
</div>
<button
onClick={() => onPlay(0)}
style={{
marginTop: 14,
border: "none",
cursor: "pointer",
background: "var(--accent)",
color: "#fff",
fontSize: 16,
fontWeight: 800,
padding: "12px 22px",
borderRadius: 999,
display: "flex",
alignItems: "center",
gap: 10,
boxShadow: "0 4px 14px oklch(70% 0.16 340 / .45)",
}}
>
<span
style={{
width: 14,
height: 16,
background: "#fff",
clipPath: "polygon(6% 0%, 100% 50%, 6% 100%)",
}}
/>
{book ? "Hörbuch abspielen" : "Alle Songs abspielen"}
</button>
</div>
<button
onClick={onClose}
aria-label="Schließen"
style={{
border: "none",
cursor: "pointer",
background: "oklch(88% 0.02 210)",
color: "var(--ink)",
fontSize: 18,
fontWeight: 900,
width: 40,
height: 40,
borderRadius: 999,
flex: "none",
}}
>
×
</button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 22 }}>
{album.tracks.map((track, index) => {
const current = currentAlbumId === album.id && currentTrackIndex === index;
return (
<button
key={index}
onClick={() => onPlay(index)}
style={{
display: "flex",
alignItems: "center",
gap: 14,
padding: "10px 14px",
borderRadius: 14,
cursor: "pointer",
border: "none",
font: "inherit",
textAlign: "left",
background: current
? "oklch(70% 0.16 340 / .16)"
: "oklch(30% 0.03 210 / .05)",
}}
>
<div
style={{
width: 30,
height: 30,
borderRadius: 999,
flex: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
font: "800 13px ui-monospace, Menlo, monospace",
background: current ? "var(--accent)" : "oklch(88% 0.02 210)",
color: current ? "#fff" : "oklch(35% 0.03 210)",
}}
>
{index + 1}
</div>
<div
style={{
flex: 1,
minWidth: 0,
fontSize: 16,
fontWeight: 800,
color: current ? "oklch(45% 0.16 340)" : "oklch(24% 0.03 210)",
}}
>
{track.title}
</div>
<div
style={{
font: "700 13px ui-monospace, Menlo, monospace",
color: "oklch(45% 0.03 210 / .7)",
}}
>
{clock(track.duration)}
</div>
</button>
);
})}
</div>
</div>
</div>
);
}