web-player: P3 — Artwork y Avatar unificados
Crea src/lib/components/ui/Artwork.svelte y Avatar.svelte (B.3) y migra todos los usos de Cover, PlaylistArt, MixArt y ArtistAvatar (Sidebar, Queue, TrackInfo, TrackContextMenu, AlbumContextMenu, TrackTitleCell, AlbumCard, PlaylistCard, MixTile, AlbumHeader, PlaylistHeader, Home, Explorar, mezcla, perfil de usuario, TopBar, MobileHeader). Borra los 4 componentes viejos. Artwork: tamaño decide dimensión y radio automáticamente (tokens cover-*/radius de P1); cascada src → mosaico (≥4 trackIds) → primera carátula → fallback, con caída en cascada si cada paso falla (antes repartida en 3 componentes con lógica ligeramente distinta). Añade un tamaño 'fill' (aspect-square w-full) no descrito en la spec, necesario para las tarjetas de grid fluidas hasta que P9 introduzca MediaCard. TrackTitleCell cambia su prop `coverSize` (clases Tailwind libres) por `size: 'sm'|'lg'` delegando en Artwork. SearchResultRow pierde su prop `kind` y el radio por tipo (ahora lo decide Artwork/Avatar). Decisión de producto (P3, sin bloquear en pregunta): se quita el nombre bajo el avatar de perfil en /u/[id] por ser redundante con el título de CollectionHeader. Small deltas de radio/tamaño documentados en README del plan (auto- radio por tamaño reemplaza clases ad-hoc; ver tabla B.1.1/B.1.2). Verificado con npm run check/build; sin QA visual autenticada (Docker no disponible en esta sesión).
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import Pause from '@lucide/svelte/icons/pause';
|
||||
import SquarePencil from '@lucide/svelte/icons/square-pen';
|
||||
import Trash from '@lucide/svelte/icons/trash';
|
||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import BackLink from '$lib/components/ui/BackLink.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||
@@ -51,12 +51,7 @@
|
||||
|
||||
<CollectionHeader eyebrow="Álbum" {title} {description} {meta}>
|
||||
{#snippet cover()}
|
||||
<PlaylistArt
|
||||
{trackIds}
|
||||
{coverUrl}
|
||||
size="large"
|
||||
class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41"
|
||||
/>
|
||||
<Artwork src={coverUrl} {trackIds} size="hero" alt={title} class="shrink-0" />
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import Alert from '$lib/components/ui/Alert.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import InfiniteScroll from '$lib/components/ui/InfiniteScroll.svelte';
|
||||
import Cover from '$lib/components/ui/Cover.svelte';
|
||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import Avatar from '$lib/components/ui/Avatar.svelte';
|
||||
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
||||
import SearchResultRow from '$lib/components/ui/SearchResultRow.svelte';
|
||||
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
||||
@@ -296,40 +296,25 @@
|
||||
{#if topResult}
|
||||
{#snippet topResultBody()}
|
||||
{#if topResult.kind === 'track'}
|
||||
<Cover
|
||||
trackId={targetId(topResult.target)}
|
||||
size="large"
|
||||
<Artwork
|
||||
trackIds={[targetId(topResult.target)]}
|
||||
size="xl"
|
||||
alt={targetTitle(topResult.target)}
|
||||
class="h-22 w-22 shrink-0 rounded-2xl"
|
||||
class="shrink-0"
|
||||
/>
|
||||
{:else if topResult.kind === 'album'}
|
||||
<PlaylistArt
|
||||
trackIds={topResult.album.coverTrackIds}
|
||||
class="h-22 w-22 shrink-0 rounded-2xl"
|
||||
/>
|
||||
<Artwork trackIds={topResult.album.coverTrackIds} size="xl" class="shrink-0" />
|
||||
{:else if topResult.kind === 'playlist'}
|
||||
<PlaylistArt
|
||||
playlistId={topResult.playlist.id}
|
||||
<Artwork
|
||||
src="/api/playlists/{topResult.playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
topResult.playlist.updatedAt
|
||||
)}"
|
||||
trackIds={topResult.playlist.coverTrackIds}
|
||||
version={topResult.playlist.updatedAt}
|
||||
class="h-22 w-22 shrink-0 rounded-2xl"
|
||||
size="xl"
|
||||
class="shrink-0"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="grid h-22 w-22 shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2"
|
||||
>
|
||||
{#if topResult.user.profilePictureUrl}
|
||||
<img
|
||||
src={topResult.user.profilePictureUrl}
|
||||
alt=""
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-xl font-semibold text-fg uppercase"
|
||||
>{topResult.user.name.charAt(0)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar name={topResult.user.name} src={topResult.user.profilePictureUrl} size="lg" />
|
||||
{/if}
|
||||
|
||||
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
||||
@@ -443,7 +428,6 @@
|
||||
{#each capped(songRows) as { target: item, seconds }, i (targetId(item))}
|
||||
<li>
|
||||
<SearchResultRow
|
||||
kind="track"
|
||||
title={targetTitle(item)}
|
||||
subtitle={targetArtist(item)}
|
||||
meta="{fmtTime(seconds)} · {fmtPlays(targetListensCount(item))}"
|
||||
@@ -452,16 +436,16 @@
|
||||
oncontextmenu={(e) => openContextMenu(e, item)}
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<Cover
|
||||
trackId={targetId(item)}
|
||||
size="small"
|
||||
<Artwork
|
||||
trackIds={[targetId(item)]}
|
||||
size="lg"
|
||||
alt={targetTitle(item)}
|
||||
class="{artClass} ring-1 ring-line ring-inset"
|
||||
>
|
||||
{#if isTargetCurrent(item)}
|
||||
<NowPlaying paused={!player.playing} />
|
||||
{/if}
|
||||
</Cover>
|
||||
</Artwork>
|
||||
{/snippet}
|
||||
</SearchResultRow>
|
||||
</li>
|
||||
@@ -495,14 +479,13 @@
|
||||
{#each capped(albums) as album (album.id)}
|
||||
<li>
|
||||
<SearchResultRow
|
||||
kind="album"
|
||||
title={album.title}
|
||||
meta="{Number(album.trackCount)} canciones"
|
||||
href="/albums/{album.id}"
|
||||
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<PlaylistArt trackIds={album.coverTrackIds} class={artClass} />
|
||||
<Artwork trackIds={album.coverTrackIds} size="lg" class={artClass} />
|
||||
{/snippet}
|
||||
</SearchResultRow>
|
||||
</li>
|
||||
@@ -525,17 +508,9 @@
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(users) as u (u.id)}
|
||||
<li>
|
||||
<SearchResultRow kind="user" title={u.name} href="/u/{u.id}">
|
||||
<SearchResultRow title={u.name} href="/u/{u.id}">
|
||||
{#snippet art(artClass)}
|
||||
<div class="{artClass} grid place-items-center bg-surface-2">
|
||||
{#if u.profilePictureUrl}
|
||||
<img src={u.profilePictureUrl} alt="" class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<span class="text-sm font-semibold text-fg uppercase">
|
||||
{u.name.charAt(0)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar name={u.name} src={u.profilePictureUrl} size="md" class={artClass} />
|
||||
{/snippet}
|
||||
</SearchResultRow>
|
||||
</li>
|
||||
@@ -559,16 +534,17 @@
|
||||
{#each capped(playlistMatches) as playlist (playlist.id)}
|
||||
<li>
|
||||
<SearchResultRow
|
||||
kind="playlist"
|
||||
title={playlist.name}
|
||||
subtitle={playlist.description}
|
||||
href="/playlists/{playlist.id}"
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<PlaylistArt
|
||||
playlistId={playlist.id}
|
||||
<Artwork
|
||||
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
trackIds={playlist.coverTrackIds}
|
||||
version={playlist.updatedAt}
|
||||
size="lg"
|
||||
class={artClass}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import BackLink from '$lib/components/ui/BackLink.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Alert from '$lib/components/ui/Alert.svelte';
|
||||
import MixArt from '$lib/components/ui/MixArt.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||
@@ -75,7 +75,12 @@
|
||||
meta="{items.length} {items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}"
|
||||
>
|
||||
{#snippet cover()}
|
||||
<MixArt {items} class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41" />
|
||||
<Artwork
|
||||
trackIds={items.map((item) => item.trackId)}
|
||||
size="hero"
|
||||
alt={mix.title}
|
||||
class="shrink-0"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
align="center"
|
||||
>
|
||||
{#snippet cover()}
|
||||
<PlaylistArt
|
||||
playlistId={id}
|
||||
<Artwork
|
||||
src="/api/playlists/{id}/cover?size=large&v={encodeURIComponent(updatedAt)}"
|
||||
{trackIds}
|
||||
size="large"
|
||||
version={updatedAt}
|
||||
class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41"
|
||||
size="hero"
|
||||
alt={name}
|
||||
class="shrink-0"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import PlaylistCard from '$lib/components/ui/PlaylistCard.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import ArtistAvatar from '$lib/components/ui/ArtistAvatar.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';
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
actions={canFollow ? followAction : undefined}
|
||||
>
|
||||
{#snippet cover()}
|
||||
<ArtistAvatar name={profile.name} imageUrl={profile.profilePictureUrl} size={144} />
|
||||
<Avatar name={profile.name} src={profile.profilePictureUrl} size="hero" />
|
||||
{/snippet}
|
||||
</CollectionHeader>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
type TrackTarget
|
||||
} from '$lib/tracks';
|
||||
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
|
||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import MixTile from '$lib/components/ui/MixTile.svelte';
|
||||
import EqBars from '$lib/components/ui/EqBars.svelte';
|
||||
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
|
||||
@@ -215,7 +215,7 @@
|
||||
title={item.title}
|
||||
artist={item.artist}
|
||||
ownerUserId={item.ownerUserId}
|
||||
coverSize="h-14 w-14"
|
||||
size="lg"
|
||||
active={isCurrent}
|
||||
onClick={() => playContinue(i)}
|
||||
/>
|
||||
@@ -250,12 +250,14 @@
|
||||
onclick={() => goto(`/playlists/${spotlight.id}`)}
|
||||
onkeydown={(e) => onRowKeydown(e, () => goto(`/playlists/${spotlight.id}`))}
|
||||
>
|
||||
<PlaylistArt
|
||||
playlistId={spotlight.id}
|
||||
<Artwork
|
||||
src="/api/playlists/{spotlight.id}/cover?size=large&v={encodeURIComponent(
|
||||
spotlight.updatedAt
|
||||
)}"
|
||||
trackIds={spotlight.coverTrackIds}
|
||||
version={spotlight.updatedAt}
|
||||
size="large"
|
||||
class="h-34 w-34 shrink-0 rounded-2xl"
|
||||
size="2xl"
|
||||
alt={spotlight.name}
|
||||
class="shrink-0"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-col gap-3">
|
||||
<p class="text-xs font-medium tracking-widest text-fg-2 uppercase">
|
||||
@@ -292,9 +294,7 @@
|
||||
? 'bg-accent-tint'
|
||||
: 'hover:bg-hover'}"
|
||||
>
|
||||
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums"
|
||||
>{i + 1}</span
|
||||
>
|
||||
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
class="truncate text-sm {isTargetCurrent(target)
|
||||
@@ -309,9 +309,7 @@
|
||||
class="mt-0.5 text-xs text-fg-3"
|
||||
/>
|
||||
</div>
|
||||
<span class="shrink-0 text-xs text-muted tabular-nums"
|
||||
>{fmtTime(seconds)}</span
|
||||
>
|
||||
<span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(seconds)}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="p-2 text-sm text-fg-3">Esta playlist todavía no tiene canciones.</p>
|
||||
@@ -369,7 +367,6 @@
|
||||
title={targetTitle(target)}
|
||||
artist={targetArtist(target)}
|
||||
ownerUserId={targetOwnerUserId(target)}
|
||||
coverSize="h-10 w-10"
|
||||
explicit={targetExplicit(target)}
|
||||
active={isCurrent}
|
||||
onClick={() => playPopular(i)}
|
||||
@@ -414,17 +411,19 @@
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="group/card animate-enter flex items-center gap-3.5 rounded-2xl p-3 transition-colors hover:bg-hover"
|
||||
style="animation-delay:{Math.min(i, 10) * 40}ms"
|
||||
class="group/card animate-enter flex items-center gap-3.5 rounded-panel p-3 transition-colors hover:bg-hover"
|
||||
style="--i:{i}"
|
||||
onclick={() => goto(`/playlists/${playlist.id}`)}
|
||||
onkeydown={(e) => onRowKeydown(e, () => goto(`/playlists/${playlist.id}`))}
|
||||
>
|
||||
<PlaylistArt
|
||||
playlistId={playlist.id}
|
||||
<Artwork
|
||||
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
trackIds={playlist.coverTrackIds}
|
||||
version={playlist.updatedAt}
|
||||
size="medium"
|
||||
class="h-15.5 w-15.5 shrink-0 rounded-control"
|
||||
size="lg"
|
||||
alt={playlist.name}
|
||||
class="shrink-0"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm tracking-tight text-fg">
|
||||
|
||||
Reference in New Issue
Block a user