feat(web-player): rebuild Inicio and Buscar to the design handoff
Applies the handoff's layout and image shapes while keeping the current Musify design system (Inter, colors, tokens) — no font or palette swap. Inicio now stacks distinct shelves instead of repeated square grids: recently played rows, a rail of recently listened albums with an "Álbum" badge, a "Hecho para ti" bento of mixes, a rail of playlists with a trailing dashed "Ver todas" tile, tall "Canciones nuevas" posters with a "Nuevo" badge, a rail of circular top-artist avatars, and a recommended rail. Buscar splits into Álbumes (square cards + badge), Usuarios (a circular-avatar directory) and a flat Canciones list. Top artists and the search user directory are filler data until the backend exposes them. New shared pieces: Rail, CoverBadge, ArtistAvatar, MixBentoTile. The orphaned TrackTile and MixCard are removed.
This commit is contained in:
@@ -1,84 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Cover from '$lib/components/ui/Cover.svelte';
|
||||
import PlayPauseOverlay from '$lib/components/ui/PlayPauseOverlay.svelte';
|
||||
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
|
||||
|
||||
interface Props {
|
||||
id: string | number;
|
||||
title: string;
|
||||
artist?: string | null;
|
||||
typeLabel?: string;
|
||||
coverSrc?: string | null;
|
||||
coverSize?: 'small' | 'medium' | 'large';
|
||||
coverShape?: 'square' | 'round';
|
||||
hue: number;
|
||||
explicit?: boolean;
|
||||
active: boolean;
|
||||
playing: boolean;
|
||||
index: number;
|
||||
onClick: () => void;
|
||||
onContextMenu?: (event: MouseEvent) => void;
|
||||
badge?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
title,
|
||||
artist,
|
||||
typeLabel,
|
||||
coverSrc,
|
||||
coverSize = 'large',
|
||||
coverShape = 'square',
|
||||
hue,
|
||||
explicit = false,
|
||||
active,
|
||||
playing,
|
||||
index,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
badge
|
||||
}: Props = $props();
|
||||
|
||||
const meta = $derived([typeLabel, artist].filter((part) => !!part).join(' · '));
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="group animate-enter relative min-w-0"
|
||||
style="animation-delay:{index * 40}ms"
|
||||
oncontextmenu={onContextMenu}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClick}
|
||||
aria-label="{active && playing ? 'Pausar' : 'Reproducir'} {title}"
|
||||
class="block w-full text-left"
|
||||
>
|
||||
<Cover
|
||||
trackId={id}
|
||||
src={coverSrc}
|
||||
{hue}
|
||||
size={coverSize}
|
||||
alt={title}
|
||||
class="aspect-square w-full shadow-art {coverShape === 'round'
|
||||
? 'rounded-art-round'
|
||||
: 'rounded-art'}"
|
||||
>
|
||||
<PlayPauseOverlay {active} {playing} />
|
||||
</Cover>
|
||||
<div class="mt-2.5 flex min-w-0 items-center gap-1.5">
|
||||
{#if explicit}
|
||||
<ExplicitBadge />
|
||||
{/if}
|
||||
<span class="truncate text-fg" {title}>{title}</span>
|
||||
</div>
|
||||
{#if badge || meta}
|
||||
<div class="mt-0.5 flex min-w-0 items-center gap-1.5">
|
||||
{@render badge?.()}
|
||||
{#if meta}
|
||||
<span class="truncate text-sm text-fg-3" title={meta}>{meta}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { gradientForHue, hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
let { id, name, size = 108 }: Props = $props();
|
||||
|
||||
const initials = $derived(
|
||||
name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase() ?? '')
|
||||
.join('')
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center gap-2.5 text-center" style="width:{size}px">
|
||||
<div
|
||||
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/artist:-translate-y-1 group-hover/artist:shadow-art-lg"
|
||||
style="width:{size}px;height:{size}px;background:{gradientForHue(hueFor(id))}"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0"
|
||||
style="background:repeating-linear-gradient(135deg, rgba(255,255,255,0.12) 0 2px, transparent 2px 10px)"
|
||||
></div>
|
||||
<span class="relative text-lg font-semibold text-white/90">{initials}</span>
|
||||
</div>
|
||||
<div class="w-full truncate text-sm text-fg">{name}</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { label, class: klass = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="pointer-events-none absolute top-2 left-2 rounded-full bg-black/45 px-2 py-0.5 text-[10px] font-semibold tracking-[0.08em] text-white uppercase backdrop-blur-sm {klass}"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import MixArt from './MixArt.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
import type { Mix } from '$lib/mixes';
|
||||
|
||||
interface Props {
|
||||
mix: Mix;
|
||||
featured?: boolean;
|
||||
banner?: boolean;
|
||||
class?: string;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
let { mix, featured = false, banner = false, class: klass = '', index = 0 }: Props = $props();
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/mixes/{mix.id}"
|
||||
class="group/tile animate-enter relative block overflow-hidden rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset hover:shadow-art-lg focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none {klass}"
|
||||
style="animation-delay:{Math.min(index, 10) * 45}ms"
|
||||
>
|
||||
<MixArt
|
||||
items={mix.items}
|
||||
hue={hueFor(mix.id)}
|
||||
class="absolute inset-0 h-full w-full transition duration-500 ease-out group-hover/tile:scale-[1.04]"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0"
|
||||
style="background:linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.32) 42%, rgba(0,0,0,0.05) 100%)"
|
||||
></div>
|
||||
<div
|
||||
class="relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
||||
? 'sm:flex-row sm:items-center sm:justify-between sm:gap-4'
|
||||
: ''}"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-semibold text-white {featured ? 'text-xl sm:text-2xl' : 'text-base'}"
|
||||
>
|
||||
{mix.title}
|
||||
</div>
|
||||
{#if mix.subtitle}
|
||||
<div class="mt-0.5 truncate text-xs text-white/70 sm:text-sm">{mix.subtitle}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MediaCard from './MediaCard.svelte';
|
||||
import MixArt from './MixArt.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
import type { Mix } from '$lib/mixes';
|
||||
|
||||
interface Props {
|
||||
mix: Mix;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
let { mix, index = 0 }: Props = $props();
|
||||
|
||||
const count = $derived(Number(mix.itemCount));
|
||||
</script>
|
||||
|
||||
<MediaCard
|
||||
href="/mixes/{mix.id}"
|
||||
title={mix.title}
|
||||
subtitle="{count} {count === 1 ? 'canción' : 'canciones'}"
|
||||
{index}
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
<MixArt items={mix.items} hue={hueFor(mix.id)} class={artClass} />
|
||||
{/snippet}
|
||||
</MediaCard>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { class: klass = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="no-scrollbar -mx-1 flex gap-4 overflow-x-auto px-1 pb-2 {klass}">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
{#if actions}
|
||||
<div class="mb-3 flex items-baseline justify-between gap-4 sm:mb-4 {klass}">
|
||||
<h2 class="text-lg font-semibold tracking-tight text-fg sm:text-xl">{title}</h2>
|
||||
<h2 class="text-lg tracking-tight text-fg sm:text-[28px]">{title}</h2>
|
||||
{@render actions()}
|
||||
</div>
|
||||
{:else}
|
||||
<h2 class="mb-3 text-lg font-semibold tracking-tight text-fg sm:mb-4 sm:text-xl {klass}">
|
||||
<h2 class="mb-3 text-lg tracking-tight text-fg sm:mb-4 sm:text-[28px] {klass}">
|
||||
{title}
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user