web-player: P7 — filas (ListRow, MediaIdentity, EqBars)

Crea MediaIdentity.svelte (renombra y generaliza TrackTitleCell,
puramente presentacional: arte + título + subtítulo, sin manejo de
click) y ListRow.svelte encima (href|onclick, active, size xs/sm/lg,
variant row|card, leading/trailing/overlay). Usa MediaIdentity también
en player/TrackInfo... (pendiente: TrackInfo se deja para P12, que ya
tiene tarea explícita de tocar ese archivo).

Migra a ListRow: SearchResultRow (Explorar, 4 grupos — se borra el
componente), Inicio (Continuar escuchando, Populares, spotlight, Tus
playlists), Queue (A continuación), Sidebar (playlists). Las 6 tablas
de pista (AlbumTrackTable, PlaylistTrackTable, library, liked, mixes,
LibraryPicker) migran a MediaIdentity con su propio use:pressable
local, ya que TrackList (P8) todavía no existe.

EqBars gana prop `overlay` (fondo scrim/45, color on-art) que
sustituye a NowPlaying.svelte (borrado). Corrige de paso 6 llamadas a
plural() que P5 había dejado sin migrar (liked, library, mixes,
playlists, PlaylistHeader, Inicio).

Deltas documentados en el registro del plan: fila activa de Sidebar
ahora usa el tinte de acento (antes gris neutro); tarjeta "Tus
playlists" de Inicio pierde la línea de autor redundante y su radio
pasa a rounded-art; filas de Queue pierden el opacity-80 del arte.

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>
This commit is contained in:
2026-09-21 00:14:20 +02:00
co-authored by Claude Sonnet 5
parent a0c9c5fca9
commit 5a528e197e
19 changed files with 407 additions and 390 deletions
@@ -21,7 +21,7 @@
import { enhance } from '$app/forms';
import type { LucideIcon } from '@lucide/svelte';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import Artwork from './Artwork.svelte';
import MediaIdentity from './MediaIdentity.svelte';
import GlassMenu from './GlassMenu.svelte';
import MenuItem from './MenuItem.svelte';
@@ -93,14 +93,14 @@
{/each}
<button
type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
class="flex w-full items-center rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
>
<Artwork
src="/api/playlists/{playlist.id}/cover?size=small"
<MediaIdentity
title={playlist.name}
coverSrc="/api/playlists/{playlist.id}/cover?size=small"
size="xs"
class="shrink-0"
titleClass="text-fg-2"
/>
<span class="truncate text-fg-2">{playlist.name}</span>
</button>
</form>
{/each}
+31 -11
View File
@@ -3,21 +3,41 @@
size?: number;
barWidth?: number;
paused?: boolean;
overlay?: boolean;
class?: string;
}
let { size = 13, barWidth = 2, paused = false, class: klass = '' }: Props = $props();
let {
size = 13,
barWidth = 2,
paused = false,
overlay = false,
class: klass = ''
}: Props = $props();
const delays = [0, 0.3, 0.6];
</script>
<div class="flex items-end justify-center gap-0.5 {klass}" style="height:{size}px">
{#each delays as delay (delay)}
<span
class="rounded-full bg-accent"
style="width:{barWidth}px; height:100%; animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
></span>
{/each}
</div>
{#if overlay}
<div class="absolute inset-0 flex items-end justify-center gap-0.75 bg-scrim/45 pb-2.5 {klass}">
{#each delays as delay (delay)}
<span
class="h-3.5 w-0.75 origin-bottom rounded-full bg-on-art"
style="animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
></span>
{/each}
</div>
{:else}
<div class="flex items-end justify-center gap-0.5 {klass}" style="height:{size}px">
{#each delays as delay (delay)}
<span
class="rounded-full bg-accent"
style="width:{barWidth}px; height:100%; animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
></span>
{/each}
</div>
{/if}
@@ -0,0 +1,90 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import MediaIdentity from './MediaIdentity.svelte';
import { pressable } from '$lib/actions/pressable';
interface Props {
href?: string;
onclick?: () => void;
oncontextmenu?: (event: MouseEvent) => void;
active?: boolean;
size?: 'xs' | 'sm' | 'lg';
variant?: 'row' | 'card';
title: string;
subtitle?: string | null;
subtitleHref?: string | number | null;
explicit?: boolean;
trackId?: string | number;
trackIds?: (string | number)[];
coverSrc?: string | null;
art?: Snippet;
overlay?: Snippet;
leading?: Snippet;
trailing?: Snippet;
class?: string;
style?: string;
}
let {
href,
onclick,
oncontextmenu,
active = false,
size = 'sm',
variant = 'row',
title,
subtitle,
subtitleHref,
explicit = false,
trackId,
trackIds,
coverSrc,
art,
overlay,
leading,
trailing,
class: klass = '',
style
}: Props = $props();
const classes = $derived(
`flex items-center gap-3 text-left transition-colors ${
variant === 'card' ? 'rounded-art p-2.25 pr-3.5' : 'rounded-control p-2'
} ${active ? 'bg-accent-tint' : 'hover:bg-hover'} ${klass}`
);
</script>
{#snippet content()}
{@render leading?.()}
<MediaIdentity
{title}
artist={subtitle}
ownerUserId={subtitleHref}
{trackId}
{trackIds}
{coverSrc}
{size}
{explicit}
{active}
{art}
{overlay}
/>
{@render trailing?.()}
{/snippet}
{#if href}
<a {href} {oncontextmenu} class={classes} {style}>
{@render content()}
</a>
{:else}
<div
role="button"
tabindex="0"
use:pressable={onclick ?? (() => {})}
{oncontextmenu}
class={classes}
{style}
>
{@render content()}
</div>
{/if}
@@ -3,25 +3,27 @@
import Artwork from './Artwork.svelte';
import ExplicitBadge from './ExplicitBadge.svelte';
import ArtistLink from './ArtistLink.svelte';
import { pressable } from '$lib/actions/pressable';
interface Props {
trackId: string | number;
trackId?: string | number;
trackIds?: (string | number)[];
title: string;
artist?: string | null;
ownerUserId?: string | number | null;
coverSrc?: string | null;
size?: 'sm' | 'lg';
size?: 'xs' | 'sm' | 'lg';
explicit?: boolean;
active?: boolean;
titleClass?: string;
onClick: () => void;
class?: string;
art?: Snippet;
overlay?: Snippet;
badge?: Snippet;
}
let {
trackId,
trackIds,
title,
artist,
ownerUserId,
@@ -30,23 +32,27 @@
explicit = false,
active = false,
titleClass = '',
onClick,
class: klass = '',
art,
overlay,
badge
}: Props = $props();
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
use:pressable={onClick}
onclick={(event) => event.stopPropagation()}
class="flex min-w-0 items-center gap-3.5 text-left"
>
<Artwork trackIds={[trackId]} src={coverSrc} {size} alt={title} class="shrink-0">
{@render overlay?.()}
</Artwork>
<div class="flex min-w-0 flex-1 items-center gap-3.5 {klass}">
{#if art}
{@render art()}
{:else if trackId !== undefined || coverSrc || trackIds?.length}
<Artwork
trackIds={trackIds ?? (trackId !== undefined ? [trackId] : [])}
src={coverSrc}
{size}
alt={title}
class="shrink-0"
>
{@render overlay?.()}
</Artwork>
{/if}
<div class="min-w-0">
<div class="flex min-w-0 items-center gap-2">
{#if explicit}
@@ -1,20 +0,0 @@
<script lang="ts">
interface Props {
paused?: boolean;
}
let { paused = false }: Props = $props();
const delays = [0, 0.3, 0.6];
</script>
<div class="absolute inset-0 flex items-end justify-center gap-0.75 bg-scrim/45 pb-2.5">
{#each delays as delay (delay)}
<span
class="h-3.5 w-0.75 origin-bottom rounded-full bg-on-art"
style="animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
></span>
{/each}
</div>
@@ -1,65 +0,0 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import ExplicitBadge from './ExplicitBadge.svelte';
interface Props {
title: string;
subtitle?: string | null;
meta?: string;
explicit?: boolean;
href?: string;
onclick?: () => void;
oncontextmenu?: (event: MouseEvent) => void;
art: Snippet<[string]>;
}
let {
title,
subtitle,
meta,
explicit = false,
href,
onclick,
oncontextmenu,
art
}: Props = $props();
const artClass = 'relative shrink-0';
</script>
{#snippet content()}
{@render art(artClass)}
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-1.75">
{#if explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-sm font-medium text-fg">{title}</span>
</div>
{#if subtitle}
<div class="mt-0.75 truncate text-xs text-fg-3">{subtitle}</div>
{/if}
</div>
{#if meta}
<span class="shrink-0 text-xs text-muted tabular-nums">{meta}</span>
{/if}
{/snippet}
{#if href}
<a
{href}
{oncontextmenu}
class="group/row flex w-full min-w-0 items-center gap-3.5 rounded-control p-2 pr-4 text-left transition hover:bg-surface"
>
{@render content()}
</a>
{:else}
<button
type="button"
{onclick}
{oncontextmenu}
class="group/row flex w-full min-w-0 items-center gap-3.5 rounded-control p-2 pr-4 text-left transition hover:bg-surface focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none"
>
{@render content()}
</button>
{/if}