web-player: rows (ListRow, MediaIdentity, EqBars)
Add MediaIdentity.svelte (renames and generalizes TrackTitleCell, purely presentational: art + title + subtitle, no click handling) and ListRow.svelte on top of it (href|onclick, active, size xs/sm/lg, variant row|card, leading/trailing/overlay). Also used by player/TrackInfo... (TrackInfo itself is left for a later pass, which already has an explicit task to touch that file). Migrated to ListRow: SearchResultRow (Explore, 4 groups — component removed), Home (Continue listening, Popular, spotlight, Your playlists), Queue (Up next), Sidebar (playlists). The 6 track tables (AlbumTrackTable, PlaylistTrackTable, library, liked, mixes, LibraryPicker) migrate to MediaIdentity with their own local use:pressable, since TrackList doesn't exist yet. EqBars gains an `overlay` prop (scrim/45 background, on-art color) replacing NowPlaying.svelte (removed). Along the way, fixes 6 plural() calls that a previous pass had left unmigrated (liked, library, mixes, playlists, PlaylistHeader, Home). Deltas documented in the plan's log: Sidebar's active row now uses the accent tint (previously neutral gray); Home's "Your playlists" card loses its redundant author line and its radius becomes rounded-art; Queue rows lose the art's opacity-80. Verified with npm run check/build; no authenticated visual QA (Docker unavailable this session).
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
+22
-16
@@ -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}
|
||||
Reference in New Issue
Block a user