test-web / test (push) Failing after 33s
First wave of Home visual polish (UI tasks 78/79/81/85): - CardActionCluster like/queue/menu cluster is now opacity-0 and fades in on group-hover or focus-within. The group class moves from the inner anchor to the outer card wrapper so the cluster (positioned outside the anchor) participates in the same hover scope. PlaylistCard refresh kebab gets the same treatment. - AlbumCard drops the year line. Title plus artist is enough on Home tiles; the album detail page still shows the year. - HorizontalScrollRow h2 picks up a flex-baseline layout with a trailing 12-wide accent rule (after:bg-accent/60), so section headers read as chapter breaks instead of identical plain text. - AlbumCard art-wrap is shadow-sm by default and lifts to shadow-lg plus ring-accent/40 on hover. Pairs with the existing group-hover:scale-1.03 for a clean lift-on-mouseover feel.
87 lines
2.6 KiB
Svelte
87 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import type { ArtistRef, TrackRef } from '$lib/api/types';
|
|
import { api } from '$lib/api/client';
|
|
import { playQueue, enqueueTracks } from '$lib/player/store.svelte';
|
|
import { Disc3, Play } from 'lucide-svelte';
|
|
import ArtistMenu from './ArtistMenu.svelte';
|
|
import CardActionCluster from './CardActionCluster.svelte';
|
|
import { listArtistTracks } from '$lib/api/artists';
|
|
|
|
let { artist }: { artist: ArtistRef } = $props();
|
|
|
|
let queueing = $state(false);
|
|
|
|
function shuffle<T>(items: T[]): T[] {
|
|
const arr = items.slice();
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
async function onPlayClick(e: MouseEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const tracks = await api.get<TrackRef[]>(`/api/artists/${artist.id}/tracks`);
|
|
if (tracks.length === 0) return;
|
|
playQueue(shuffle(tracks), 0);
|
|
}
|
|
|
|
async function onAddToQueue(e: MouseEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (queueing) return;
|
|
queueing = true;
|
|
try {
|
|
const tracks = await listArtistTracks(artist.id);
|
|
if (tracks.length > 0) enqueueTracks(tracks);
|
|
} finally {
|
|
queueing = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="card group relative">
|
|
<a
|
|
href={`/artists/${artist.id}`}
|
|
class="block rounded focus-visible:ring-2 focus-visible:ring-accent"
|
|
>
|
|
<div class="art-wrap relative mx-auto aspect-square w-full overflow-hidden rounded-full bg-surface-hover">
|
|
{#if artist.cover_url}
|
|
<img
|
|
src={artist.cover_url}
|
|
alt=""
|
|
class="h-full w-full object-cover transition-transform group-hover:scale-[1.03]"
|
|
loading="lazy"
|
|
/>
|
|
{:else}
|
|
<div class="flex h-full w-full items-center justify-center">
|
|
<Disc3 strokeWidth={1} class="h-4/5 w-4/5 text-text-muted" />
|
|
</div>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
aria-label={`Play ${artist.name}`}
|
|
onclick={onPlayClick}
|
|
class="play-overlay absolute inset-0 m-auto flex h-12 w-12 items-center justify-center rounded-full"
|
|
>
|
|
<Play size={24} strokeWidth={1.5} fill="currentColor" />
|
|
</button>
|
|
</div>
|
|
<div class="mt-2 truncate text-center text-sm font-medium text-text-primary">{artist.name}</div>
|
|
</a>
|
|
<CardActionCluster
|
|
likeEntityType="artist"
|
|
likeEntityId={artist.id}
|
|
onAdd={onAddToQueue}
|
|
addLabel={`Add ${artist.name} to queue`}
|
|
addDisabled={queueing}
|
|
>
|
|
{#snippet menu()}
|
|
<ArtistMenu artistId={artist.id} artistName={artist.name} />
|
|
{/snippet}
|
|
</CardActionCluster>
|
|
</div>
|
|
|