web-player: cards and grids (MediaCard)

MediaCard now builds its own art (src/trackIds via Artwork
size="fill", or a custom art snippet) and its own play button
(onPlay, with a scrim and a glass PlayButton on hover) instead of
relying on each wrapper passing it a snippet. Replaces AlbumCard,
PlaylistCard and MixTile, which are removed along with
MediaGrid.svelte (their grids move to the
grid-cards/grid-cards-lg/grid-tiles/grid-wide @utility recipes).

format.ts gains albumMeta() and playlistMeta() (previously ad-hoc
calculations inside each card). Explore's genre tile moves from an
inline style= gradient to the genre-tile @utility recipe +
--tile-hue. Along the way, fixes the "follower/s" pluralization on
the user profile that had been missed.

Delta documented: the art's hover-zoom and the scrim+PlayButton
(previously exclusive to MixTile) become MediaCard behavior gated on
the onPlay prop — they'll apply to any future card that uses it, for
consistency.

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
This commit is contained in:
2026-09-21 00:30:30 +02:00
parent 8cea298b2a
commit 964dfc809f
13 changed files with 126 additions and 221 deletions
@@ -3,12 +3,12 @@
import Disc from '@lucide/svelte/icons/disc-3';
import Page from '$lib/components/ui/Page.svelte';
import PageHeader from '$lib/components/ui/PageHeader.svelte';
import MediaGrid from '$lib/components/ui/MediaGrid.svelte';
import AlbumCard from '$lib/components/ui/AlbumCard.svelte';
import MediaCard from '$lib/components/ui/MediaCard.svelte';
import AlbumForm from '$lib/components/ui/AlbumForm.svelte';
import Modal from '$lib/components/ui/Modal.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { albumMeta } from '$lib/format';
let { data, form } = $props();
@@ -35,19 +35,21 @@
</PageHeader>
{#if items.length > 0}
<MediaGrid class="mt-7 sm:mt-8">
<div class="mt-7 grid-cards sm:mt-8">
{#each items as album, i (album.id)}
<AlbumCard
id={album.id}
<MediaCard
href="/albums/{album.id}"
title={album.title}
releaseYear={album.releaseYear === null ? undefined : Number(album.releaseYear)}
trackCount={Number(album.trackCount)}
subtitle={albumMeta(
album.releaseYear === null ? undefined : Number(album.releaseYear),
Number(album.trackCount)
)}
trackIds={album.coverTrackIds}
coverSrc="/api/albums/{album.id}/cover?size=large"
src="/api/albums/{album.id}/cover?size=large"
index={i}
/>
{/each}
</MediaGrid>
</div>
{:else}
<EmptyState
icon={Disc}