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). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
61 lines
1.1 KiB
Svelte
61 lines
1.1 KiB
Svelte
<script lang="ts">
|
|
type Size = 'sm' | 'md' | 'lg' | 'hero';
|
|
|
|
interface Props {
|
|
name: string;
|
|
src?: string | null;
|
|
size: Size;
|
|
class?: string;
|
|
}
|
|
|
|
let { name, src, size, class: klass = '' }: Props = $props();
|
|
|
|
const DIMENSION: Record<Size, string> = {
|
|
sm: 'size-cover-xs',
|
|
md: 'size-cover-lg',
|
|
lg: 'size-cover-xl',
|
|
hero: 'size-cover-hero'
|
|
};
|
|
|
|
const TEXT: Record<Size, string> = {
|
|
sm: 'text-sm',
|
|
md: 'text-base',
|
|
lg: 'text-xl',
|
|
hero: 'text-3xl'
|
|
};
|
|
|
|
const initials = $derived(
|
|
name
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
.join('')
|
|
);
|
|
|
|
let failed = $state(false);
|
|
$effect(() => {
|
|
src;
|
|
failed = false;
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2 {DIMENSION[
|
|
size
|
|
]} {klass}"
|
|
>
|
|
{#if src && !failed}
|
|
<img
|
|
{src}
|
|
alt=""
|
|
loading="lazy"
|
|
decoding="async"
|
|
onerror={() => (failed = true)}
|
|
class="absolute inset-0 h-full w-full object-cover"
|
|
/>
|
|
{:else}
|
|
<span class="font-semibold text-fg uppercase {TEXT[size]}">{initials}</span>
|
|
{/if}
|
|
</div>
|