web-player: P9 — tarjetas y grids (MediaCard)

MediaCard pasa a construir su propio arte (src/trackIds vía Artwork
size="fill", o un snippet art a medida) y su botón de reproducir
(onPlay, con scrim y PlayButton glass en hover) en vez de depender de
que cada wrapper le pase un snippet. Sustituye a AlbumCard,
PlaylistCard y MixTile, que se borran junto con MediaGrid.svelte (sus
grids pasan a los @utility grid-cards/grid-cards-lg/grid-tiles/
grid-wide de P1).

format.ts gana albumMeta() y playlistMeta() (antes cálculos ad-hoc
dentro de cada tarjeta). El tile de género en Explorar pasa de un
style= con el gradiente inline a la receta @utility genre-tile
+ --tile-hue. De paso, corrige la pluralización "seguidor/es" del
perfil de usuario que quedó suelta.

Delta documentado: el hover-zoom del arte y el scrim+PlayButton (antes
exclusivos de MixTile) pasan a ser comportamiento de MediaCard
condicionado a la prop onPlay — se aplicarían a cualquier tarjeta
futura que la use, por consistencia.

Verificado con npm run check/build; sin QA visual autenticada (Docker
no disponible en esta sesión).
This commit is contained in:
2026-09-21 00:30:30 +02:00
parent 8c6d4d6a6d
commit a0ba57b569
13 changed files with 126 additions and 221 deletions
+16 -12
View File
@@ -3,13 +3,13 @@
import { enhance } from '$app/forms';
import ListMusic from '@lucide/svelte/icons/list-music';
import Page from '$lib/components/ui/Page.svelte';
import MediaGrid from '$lib/components/ui/MediaGrid.svelte';
import PlaylistCard from '$lib/components/ui/PlaylistCard.svelte';
import MediaCard from '$lib/components/ui/MediaCard.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Avatar from '$lib/components/ui/Avatar.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import { plural, playlistMeta } from '$lib/format';
let { data, form } = $props();
@@ -58,9 +58,11 @@
<CollectionHeader
eyebrow="Perfil"
title={profile.name}
meta="{profile.followersCount} {profile.followersCount === 1
? 'seguidor'
: 'seguidores'} · {profile.followingCount} siguiendo"
meta="{plural(
Number(profile.followersCount),
'seguidor',
'seguidores'
)} · {profile.followingCount} siguiendo"
align="center"
actions={canFollow ? followAction : undefined}
>
@@ -73,18 +75,20 @@
<SectionHeading title="Playlists públicas" subtitle="Creadas y compartidas por este usuario" />
{#if playlists.length > 0}
<MediaGrid min="210px" minMobile="180px" class="mt-6">
<div class="mt-6 grid-cards-lg">
{#each playlists as playlist, i (playlist.id)}
<PlaylistCard
id={playlist.id}
name={playlist.name}
trackCount={playlist.trackCount}
<MediaCard
href="/playlists/{playlist.id}"
title={playlist.name}
subtitle={playlistMeta(Number(playlist.trackCount))}
trackIds={playlist.coverTrackIds}
updatedAt={playlist.updatedAt}
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
playlist.updatedAt
)}"
index={i}
/>
{/each}
</MediaGrid>
</div>
{:else}
<EmptyState
icon={ListMusic}