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). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MediaCard from './MediaCard.svelte';
|
||||
import Artwork from './Artwork.svelte';
|
||||
import { plural } from '$lib/format';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
title: string;
|
||||
href?: string;
|
||||
subtitle?: string;
|
||||
typeLabel?: string;
|
||||
releaseYear?: number;
|
||||
trackCount?: number;
|
||||
trackIds?: (string | number)[];
|
||||
coverSrc?: string | null;
|
||||
index?: number;
|
||||
onContextMenu?: (event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
title,
|
||||
href,
|
||||
subtitle,
|
||||
typeLabel,
|
||||
releaseYear,
|
||||
trackCount,
|
||||
trackIds = [],
|
||||
coverSrc,
|
||||
index = 0,
|
||||
onContextMenu
|
||||
}: Props = $props();
|
||||
|
||||
const meta = $derived(
|
||||
[
|
||||
typeLabel,
|
||||
subtitle,
|
||||
releaseYear ? String(releaseYear) : undefined,
|
||||
trackCount === undefined ? undefined : plural(trackCount, 'canción', 'canciones')
|
||||
]
|
||||
.filter((part) => !!part)
|
||||
.join(' · ')
|
||||
);
|
||||
</script>
|
||||
|
||||
<MediaCard
|
||||
href={href ?? `/albums/${id}`}
|
||||
{title}
|
||||
subtitle={meta}
|
||||
{index}
|
||||
oncontextmenu={onContextMenu}
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<Artwork src={coverSrc} {trackIds} size="fill" alt={title} class={artClass} />
|
||||
{/snippet}
|
||||
</MediaCard>
|
||||
@@ -1,30 +1,59 @@
|
||||
<script lang="ts" module>
|
||||
export const mediaCardArt =
|
||||
'aspect-square w-full rounded-art transition duration-300 ease-out';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Artwork from './Artwork.svelte';
|
||||
import PlayButton from './PlayButton.svelte';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
index?: number;
|
||||
src?: string | null;
|
||||
trackIds?: (string | number)[];
|
||||
onPlay?: (event: MouseEvent) => void;
|
||||
art?: Snippet;
|
||||
oncontextmenu?: (event: MouseEvent) => void;
|
||||
art: Snippet<[string]>;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
let { href, title, subtitle, index = 0, oncontextmenu, art }: Props = $props();
|
||||
let {
|
||||
href,
|
||||
title,
|
||||
subtitle,
|
||||
src,
|
||||
trackIds = [],
|
||||
onPlay,
|
||||
art,
|
||||
oncontextmenu,
|
||||
index = 0
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
class="group animate-enter min-w-0 text-left"
|
||||
style="animation-delay:{index * 45}ms"
|
||||
{oncontextmenu}
|
||||
>
|
||||
{@render art(mediaCardArt)}
|
||||
<a {href} class="group animate-enter min-w-0 text-left" style="--i:{index}" {oncontextmenu}>
|
||||
<div class="relative">
|
||||
{#if art}
|
||||
{@render art()}
|
||||
{:else}
|
||||
<Artwork
|
||||
{src}
|
||||
{trackIds}
|
||||
size="fill"
|
||||
alt={title}
|
||||
class="rounded-art transition duration-300 group-hover:scale-105"
|
||||
/>
|
||||
{/if}
|
||||
{#if onPlay}
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 rounded-art bg-linear-to-b from-transparent from-45% to-scrim/40"
|
||||
></div>
|
||||
<PlayButton
|
||||
variant="glass"
|
||||
size="sm"
|
||||
onclick={onPlay}
|
||||
label="Reproducir {title}"
|
||||
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2.5 truncate text-sm text-fg">{title}</div>
|
||||
{#if subtitle}
|
||||
<div class="mt-0.5 truncate text-xs text-fg-3">{subtitle}</div>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
as?: 'ul' | 'div';
|
||||
min?: string;
|
||||
minMobile?: string;
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
as = 'div',
|
||||
min = '165px',
|
||||
minMobile = '150px',
|
||||
class: klass = '',
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
class="grid media-grid gap-3 sm:gap-4 {klass}"
|
||||
style="--mf-grid-min:{min}; --mf-grid-min-mobile:{minMobile}"
|
||||
>
|
||||
{@render children()}
|
||||
</svelte:element>
|
||||
@@ -1,53 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Artwork from './Artwork.svelte';
|
||||
import PlayButton from './PlayButton.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { queueItemForTarget } from '$lib/tracks';
|
||||
import { mixItemTarget, type Mix } from '$lib/mixes';
|
||||
|
||||
interface Props {
|
||||
mix: Mix;
|
||||
index?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { mix, index = 0, class: klass = '' }: Props = $props();
|
||||
|
||||
const trackIds = $derived(mix.items.map((item) => item.trackId));
|
||||
|
||||
function play(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
const items = mix.items.map((item) => queueItemForTarget(mixItemTarget(item)));
|
||||
player.playQueue(items, 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/mixes/{mix.id}"
|
||||
class="group/tile animate-enter block cursor-pointer {klass}"
|
||||
style="--i:{index}"
|
||||
>
|
||||
<Artwork
|
||||
{trackIds}
|
||||
size="fill"
|
||||
alt={mix.title}
|
||||
class="rounded-art transition duration-300 group-hover/tile:scale-105"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent from-45% to-scrim/40"
|
||||
></div>
|
||||
<PlayButton
|
||||
variant="glass"
|
||||
size="sm"
|
||||
onclick={play}
|
||||
label="Reproducir {mix.title}"
|
||||
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity duration-150 group-hover/tile:opacity-100"
|
||||
/>
|
||||
</Artwork>
|
||||
<div class="mt-3.25 truncate text-sm font-semibold text-fg">
|
||||
{mix.title}
|
||||
</div>
|
||||
{#if mix.subtitle}
|
||||
<div class="mt-1 truncate text-xs text-fg-3">{mix.subtitle}</div>
|
||||
{/if}
|
||||
</a>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MediaCard from './MediaCard.svelte';
|
||||
import Artwork from './Artwork.svelte';
|
||||
import { plural } from '$lib/format';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
trackCount: number;
|
||||
trackIds: (string | number)[];
|
||||
updatedAt: string;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
let { id, name, trackCount, trackIds, updatedAt, index = 0 }: Props = $props();
|
||||
|
||||
const subtitle = $derived(plural(trackCount, 'canción', 'canciones'));
|
||||
</script>
|
||||
|
||||
<MediaCard href="/playlists/{id}" title={name} {subtitle} {index}>
|
||||
{#snippet art(artClass)}
|
||||
<Artwork
|
||||
src="/api/playlists/{id}/cover?size=large&v={encodeURIComponent(updatedAt)}"
|
||||
{trackIds}
|
||||
size="fill"
|
||||
alt={name}
|
||||
class={artClass}
|
||||
/>
|
||||
{/snippet}
|
||||
</MediaCard>
|
||||
Reference in New Issue
Block a user