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:
@@ -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}
|
||||
|
||||
@@ -378,13 +378,12 @@
|
||||
|
||||
{#if !data.query}
|
||||
<!-- GÉNEROS -->
|
||||
<div class="mt-7 grid grid-cols-[repeat(auto-fill,minmax(268px,1fr))] gap-3.5">
|
||||
<div class="mt-7 grid-wide">
|
||||
{#each genreTiles as genre, i (genre.query)}
|
||||
<a
|
||||
href={searchHref(genre.query)}
|
||||
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-110"
|
||||
style="animation-delay:{Math.min(i, 10) *
|
||||
40}ms; background:linear-gradient(140deg, oklch(var(--mf-tile-l) var(--mf-tile-c) {genre.hue}), oklch(var(--mf-tile-shade-l) var(--mf-tile-shade-c) {genre.hue}))"
|
||||
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-panel p-4.25 transition-[filter] genre-tile hover:brightness-110"
|
||||
style="--i:{i}; --tile-hue:{genre.hue}"
|
||||
>
|
||||
<div class="text-on-art">
|
||||
{genre.label}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
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 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 { createPlaylistModal } from '$lib/playlists.svelte';
|
||||
import { fmtDurationLong, plural } from '$lib/format';
|
||||
import { fmtDurationLong, plural, playlistMeta } from '$lib/format';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -36,18 +35,20 @@
|
||||
</PageHeader>
|
||||
|
||||
{#if items.length > 0}
|
||||
<MediaGrid min="210px" minMobile="180px" class="mt-7 sm:mt-8">
|
||||
<div class="mt-7 grid-cards-lg sm:mt-8">
|
||||
{#each items 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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user