Unify web-player design tokens and componentize collection headers

Migrates remaining one-off colors (bg-white/[N%], border-white/N, hardcoded
shadows/gradients) to the existing --mf-* tokens so light mode reacts
consistently everywhere, including a fix to --color-hover which was
hardcoded instead of pointing at --mf-surface-2. Extracts CollectionHeader
(album/playlist/mix/liked headers) and AccountMenu (TopBar/MobileHeader)
to remove five and two duplicated implementations respectively, and
normalizes inconsistent empty-state copy.
This commit is contained in:
2026-09-15 17:53:47 +02:00
parent 7861600081
commit f1756468bc
40 changed files with 463 additions and 448 deletions
@@ -6,6 +6,7 @@
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
interface Props {
title: string;
@@ -48,45 +49,34 @@
label={isOwner ? 'Volver a tus álbumes' : 'Volver al inicio'}
/>
<div class="mt-3 flex flex-col gap-5 sm:mt-4 sm:flex-row sm:items-end sm:gap-6">
<PlaylistArt
{trackIds}
{coverUrl}
size="large"
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
/>
<div class="min-w-0 flex-1">
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Álbum</p>
<h1
class="mt-1.5 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
>
{title}
</h1>
{#if description}
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
{/if}
<p class="mt-3 text-sm text-muted">{meta}</p>
</div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
{#if playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
{#if isOwner}
<Button size="sm" variant="secondary" onclick={onEdit}>
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
Editar
<CollectionHeader eyebrow="Álbum" {title} {description} {meta}>
{#snippet cover()}
<PlaylistArt
{trackIds}
{coverUrl}
size="large"
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
{#if playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
<Button size="sm" variant="secondary" onclick={onDelete}>
<Trash class="h-4 w-4" strokeWidth={1.5} />
Eliminar
</Button>
{/if}
</div>
{#if isOwner}
<Button size="sm" variant="secondary" onclick={onEdit}>
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
Editar
</Button>
<Button size="sm" variant="secondary" onclick={onDelete}>
<Trash class="h-4 w-4" strokeWidth={1.5} />
Eliminar
</Button>
{/if}
{/snippet}
</CollectionHeader>
@@ -8,6 +8,7 @@
import Button from '$lib/components/ui/Button.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
@@ -79,48 +80,40 @@
<Page>
<BackLink href="/explore" label="Volver a explorar" />
<div class="mt-3 flex flex-col gap-5 sm:mt-4 sm:flex-row sm:items-end sm:gap-6">
<Cover
trackId={album.albumId}
src={album.thumbnailUrl}
size="large"
alt={album.title}
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
oncontextmenu={(e) => (albumMenu = albumContextMenuStateFor(e, 'youtube', album.albumId))}
/>
<div class="min-w-0 flex-1">
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">
{album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'}
</p>
<h1
class="mt-1.5 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
>
{album.title}
</h1>
<p class="mt-3 text-base text-fg-2">{album.artist}</p>
<p class="mt-1 text-sm text-muted">
{[
album.releaseYear ? String(album.releaseYear) : undefined,
`${tracks.length} ${tracks.length === 1 ? 'canción' : 'canciones'}`,
fmtTime(Number(data.album.totalDurationSeconds))
]
.filter((part) => !!part)
.join(' · ')}
</p>
</div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
</div>
<CollectionHeader
eyebrow={album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'}
title={album.title}
description={album.artist}
meta={[
album.releaseYear ? String(album.releaseYear) : undefined,
`${tracks.length} ${tracks.length === 1 ? 'canción' : 'canciones'}`,
fmtTime(Number(data.album.totalDurationSeconds))
]
.filter((part) => !!part)
.join(' · ')}
>
{#snippet cover()}
<Cover
trackId={album.albumId}
src={album.thumbnailUrl}
size="large"
alt={album.title}
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
oncontextmenu={(e) => (albumMenu = albumContextMenuStateFor(e, 'youtube', album.albumId))}
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
{/snippet}
</CollectionHeader>
{#if form?.message}
<Alert tone="danger" class="mt-4">{form.message}</Alert>
@@ -313,7 +313,7 @@
Resultados de búsqueda
</div>
<h1
class="font-display text-2xl font-semibold tracking-[-0.03em] text-pretty text-fg sm:text-[28px]"
class="font-display text-2xl font-semibold tracking-[-0.03em] text-pretty text-fg sm:text-3xl"
>
Resultados para «{data.query}»
</h1>
@@ -331,13 +331,11 @@
class="flex items-center gap-1.75 rounded-full px-3.5 py-1.75 text-xs font-medium transition {sfilter ===
chip.label
? 'bg-cta-strong text-ink'
: 'bg-white/[3.5%] text-fg-2 hover:bg-white/7 hover:text-fg'}"
: 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg'}"
>
<span>{chip.label}</span>
<span
class="text-[10.5px] tabular-nums {sfilter === chip.label
? 'opacity-55'
: 'text-muted'}">{chip.count}</span
<span class="text-xs tabular-nums {sfilter === chip.label ? 'opacity-55' : 'text-muted'}"
>{chip.count}</span
>
</button>
{/each}
@@ -394,9 +392,7 @@
{/if}
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
<div
class="text-xs leading-none font-semibold tracking-[0.14em] text-fg-3 uppercase"
>
<div class="text-xs leading-none font-semibold tracking-[0.14em] text-fg-3 uppercase">
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
</div>
<div
@@ -456,7 +452,7 @@
<h1 class="font-display text-xl font-semibold tracking-[-0.025em] text-fg">
Sin resultados para «{data.query}»
</h1>
<p class="mt-2.5 text-[13px] leading-[1.65] text-fg-2">
<p class="mt-2.5 text-sm leading-[1.65] text-fg-2">
Revisa la ortografía o prueba con un término más corto. También puedes buscar por álbum o
por playlist.
</p>
@@ -464,7 +460,7 @@
{#each genreTiles.slice(0, 4) as genre (genre.query)}
<a
href={buildHref(genre.query)}
class="rounded-full border border-white/8 px-3.5 py-2 text-xs text-fg-2 transition-colors hover:bg-white/5 hover:text-fg"
class="rounded-full border border-line-strong px-3.5 py-2 text-xs text-fg-2 transition-colors hover:bg-hover hover:text-fg"
>
{genre.label}
</a>
@@ -481,12 +477,12 @@
href={buildHref(genre.query)}
class="animate-enter group relative block h-28 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-125"
style="animation-delay:{Math.min(i, 10) *
40}ms; background:linear-gradient(140deg, oklch(0.24 0.07 {genre.hue}), rgba(9,9,12,.95))"
40}ms; background:linear-gradient(140deg, oklch(0.24 0.07 {genre.hue}), var(--mf-ink))"
>
<div class="font-display text-[15.5px] font-semibold tracking-[-0.02em] text-fg">
<div class="font-display text-base font-semibold tracking-[-0.02em] text-fg">
{genre.label}
</div>
<div class="mt-1.5 text-[11.5px] text-fg-3">{genre.tagline}</div>
<div class="mt-1.5 text-xs text-fg-3">{genre.tagline}</div>
</a>
{/each}
</div>
@@ -500,7 +496,7 @@
<SectionHeading title="Canciones">
{#snippet actions()}
<div class="h-px flex-1 self-center bg-line"></div>
<span class="shrink-0 text-[11px] text-muted tabular-nums">
<span class="shrink-0 text-xs text-muted tabular-nums">
{searchCounts.Canciones}
{searchCounts.Canciones === 1 ? 'Resultado' : 'Resultados'}
</span>
@@ -553,7 +549,7 @@
<SectionHeading title="Álbumes">
{#snippet actions()}
<div class="h-px flex-1 self-center bg-line"></div>
<span class="shrink-0 text-[11px] text-muted tabular-nums">
<span class="shrink-0 text-xs text-muted tabular-nums">
{searchCounts.Álbumes}
{searchCounts.Álbumes === 1 ? 'Resultado' : 'Resultados'}
</span>
@@ -605,7 +601,7 @@
<SectionHeading title="Usuarios">
{#snippet actions()}
<div class="h-px flex-1 self-center bg-line"></div>
<span class="shrink-0 text-[11px] text-muted tabular-nums">
<span class="shrink-0 text-xs text-muted tabular-nums">
{searchCounts.Usuarios}
{searchCounts.Usuarios === 1 ? 'Resultado' : 'Resultados'}
</span>
@@ -638,7 +634,7 @@
<SectionHeading title="Playlists">
{#snippet actions()}
<div class="h-px flex-1 self-center bg-line"></div>
<span class="shrink-0 text-[11px] text-muted tabular-nums">
<span class="shrink-0 text-xs text-muted tabular-nums">
{searchCounts.Playlists}
{searchCounts.Playlists === 1 ? 'Resultado' : 'Resultados'}
</span>
+25 -31
View File
@@ -7,6 +7,7 @@
import { liked } from '$lib/player/liked.svelte';
import { fmtDate } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import Button from '$lib/components/ui/Button.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
@@ -46,38 +47,31 @@
</svelte:head>
<Page>
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6">
<div
class="grid h-36 w-36 shrink-0 place-items-center rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
style="background:linear-gradient(150deg, oklch(0.4 0.09 330), oklch(0.18 0.04 10))"
>
<Heart class="h-10 w-10 text-white/90" fill="currentColor" strokeWidth={0} />
</div>
<div class="min-w-0 flex-1">
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Colección</p>
<h1
class="mt-3 font-display text-[28px] font-semibold tracking-[-0.035em] text-fg sm:text-[33px]"
<CollectionHeader
eyebrow="Colección"
title="Me gusta"
meta="{tracks.length} {tracks.length === 1 ? 'canción' : 'canciones'}"
align="center"
>
{#snippet cover()}
<div
class="grid h-36 w-36 shrink-0 place-items-center rounded-[18px] bg-[image:var(--mf-liked-grad)] shadow-cover-lg sm:h-41 sm:w-41"
>
Me gusta
</h1>
<p class="mt-3.25 text-[12.5px] text-fg-2">
{tracks.length}
{tracks.length === 1 ? 'canción' : 'canciones'}
</p>
</div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
</div>
<Heart class="h-10 w-10 text-on-art/90" fill="currentColor" strokeWidth={0} />
</div>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
{/snippet}
</CollectionHeader>
{#if tracks.length > 0}
<TrackTable
@@ -10,6 +10,7 @@
import Button from '$lib/components/ui/Button.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import MixArt from '$lib/components/ui/MixArt.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
@@ -67,43 +68,31 @@
<Page>
<BackLink href="/" label="Volver al inicio" />
<div class="mt-3 flex flex-col gap-5 sm:mt-4 sm:flex-row sm:items-end sm:gap-6">
<MixArt
{items}
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
/>
<div class="min-w-0 flex-1">
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Mezcla</p>
<h1
class="mt-1.5 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
>
{mix.title}
</h1>
{#if mix.subtitle}
<p class="mt-3 text-base text-fg-2">{mix.subtitle}</p>
{/if}
<p class="mt-1 text-sm text-muted">
{items.length}
{items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}
</p>
</div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
<Button variant="secondary" size="sm" onclick={playShuffled} disabled={queue.length === 0}>
<Shuffle class="h-4 w-4" />
Aleatorio
</Button>
</div>
<CollectionHeader
eyebrow="Mezcla"
title={mix.title}
description={mix.subtitle ?? undefined}
meta="{items.length} {items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}"
>
{#snippet cover()}
<MixArt {items} class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41" />
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
<Button variant="secondary" size="sm" onclick={playShuffled} disabled={queue.length === 0}>
<Shuffle class="h-4 w-4" />
Aleatorio
</Button>
{/snippet}
</CollectionHeader>
{#if form?.message}
<Alert tone="danger" class="mt-4">{form.message}</Alert>
@@ -82,7 +82,7 @@
<h2 class="font-display text-xl font-semibold tracking-[-0.02em] text-fg">
¿Eliminar «{playlist.name}»?
</h2>
<p class="mt-2.5 text-[13px] leading-[1.65] text-fg-2">
<p class="mt-2.5 text-sm leading-[1.65] text-fg-2">
Esta acción no se puede deshacer, la playlist se eliminará de tu biblioteca y de todos los
dispositivos donde la tengas guardada.
</p>
@@ -1,10 +1,7 @@
<script lang="ts">
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import SquarePencil from '@lucide/svelte/icons/square-pen';
import Trash from '@lucide/svelte/icons/trash';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Button from '$lib/components/ui/Button.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
interface Props {
id: string;
@@ -31,47 +28,39 @@
onEdit,
onDelete
}: Props = $props();
const meta = $derived(
`${trackIds.length} ${trackIds.length === 1 ? 'canción' : 'canciones'} · ${
visibility === 'Public' ? 'Pública' : 'Privada'
}`
);
</script>
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6">
<PlaylistArt
playlistId={id}
{trackIds}
size="large"
version={updatedAt}
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
/>
<div class="min-w-0 flex-1">
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Lista</p>
<h1
class="mt-3 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
>
{name}
</h1>
{#if description}
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
{/if}
<p class="mt-3.25 text-[12.5px] text-fg-2">
{trackIds.length}
{trackIds.length === 1 ? 'canción' : 'canciones'} · {visibility === 'Public'
? 'Pública'
: 'Privada'}
</p>
</div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
{#if playing}
Pausar
{:else}
Reproducir
{/if}
</Button>
<Button size="sm" variant="secondary" onclick={onEdit}>
Editar
</Button>
<Button size="sm" variant="secondary" onclick={onDelete}>
Eliminar
</Button>
</div>
<CollectionHeader
eyebrow="Lista"
title={name}
description={description ?? undefined}
{meta}
align="center"
>
{#snippet cover()}
<PlaylistArt
playlistId={id}
{trackIds}
size="large"
version={updatedAt}
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
{#if playing}
Pausar
{:else}
Reproducir
{/if}
</Button>
<Button size="sm" variant="secondary" onclick={onEdit}>Editar</Button>
<Button size="sm" variant="secondary" onclick={onDelete}>Eliminar</Button>
{/snippet}
</CollectionHeader>
@@ -32,13 +32,13 @@
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6">
<ArtistAvatar name={profile.name} imageUrl={profile.profilePictureUrl} size={144} />
<div class="min-w-0 flex-1">
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Perfil</p>
<p class="text-xs font-semibold tracking-[0.16em] text-fg-2 uppercase">Perfil</p>
<h1
class="mt-3 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
class="mt-3 font-display text-3xl font-semibold tracking-[-0.035em] break-words text-fg sm:text-4xl"
>
{profile.name}
</h1>
<p class="mt-3.25 text-[12.5px] text-fg-2">
<p class="mt-3.25 text-sm text-fg-2">
{profile.followersCount}
{profile.followersCount === 1 ? 'seguidor' : 'seguidores'} · {profile.followingCount} siguiendo
</p>
@@ -115,7 +115,7 @@
class="grid h-7 w-7 shrink-0 place-items-center rounded-full text-xs font-bold {stepStates[
i
] === 'idle'
? 'bg-white/[3.5%] text-fg-2'
? 'bg-surface-2 text-fg-2'
: 'bg-accent-btn text-accent-soft'}"
>
{#if stepStates[i] === 'done'}
@@ -228,7 +228,7 @@
{#if coverPreview !== ''}
<img src={coverPreview} alt="Portada" class="h-full w-full object-cover" />
{:else}
<ImageIcon class="h-11 w-11 text-white/70" strokeWidth={1.3} />
<ImageIcon class="h-11 w-11 text-on-art/70" strokeWidth={1.3} />
{/if}
</label>
</Field>
+1 -1
View File
@@ -7,7 +7,7 @@
<h1 class="mb-2.5 font-display text-xl font-semibold tracking-[-0.02em] text-fg">
{page.status === 404 ? 'Página no encontrada' : 'No se pudo cargar tu música'}
</h1>
<p class="mb-6.5 text-[13px] leading-[1.65] text-fg-2">
<p class="mb-6.5 text-sm leading-[1.65] text-fg-2">
{page.status === 404
? 'Comprueba el enlace o vuelve al inicio.'
: 'La conexión se interrumpió. Lo que tengas descargado seguirá disponible sin conexión.'}
+30 -34
View File
@@ -152,11 +152,11 @@
>
<div class="animate-enter max-w-135 min-w-65">
<h1
class="font-display text-[28px] leading-[1.1] font-semibold tracking-[-0.035em] text-pretty text-fg sm:text-[36px]"
class="font-display text-3xl leading-[1.1] font-semibold tracking-[-0.035em] text-pretty text-fg sm:text-4xl"
>
{data.greeting}
</h1>
<p class="mt-3.5 max-w-108 text-[13.5px] leading-[1.65] text-fg-2">
<p class="mt-3.5 max-w-108 text-sm leading-[1.65] text-fg-2">
Sube lo tuyo, descubre lo nuevo y escúchalo todo en un solo sitio.
</p>
<div class="mt-6.5 flex flex-wrap items-center gap-2.5">
@@ -177,22 +177,22 @@
{#await data.listeningStats then stats}
<div class="flex gap-6.5">
<div>
<div class="font-display text-[25px] font-semibold tracking-[-0.03em] text-fg">
<div class="font-display text-2xl font-semibold tracking-[-0.03em] text-fg">
{stats.tracksThisWeek}
</div>
<div class="mt-1 text-[11.5px] text-fg-3">Canciones nuevas escuchadas esta semana</div>
<div class="mt-1 text-xs text-fg-3">Canciones nuevas escuchadas esta semana</div>
</div>
<div>
<div class="font-display text-[25px] font-semibold tracking-[-0.03em] text-fg">
<div class="font-display text-2xl font-semibold tracking-[-0.03em] text-fg">
{fmtDurationLong(Number(stats.secondsThisWeek))}
</div>
<div class="mt-1 text-[11.5px] text-fg-3">Tiempo de escucha esta semana</div>
<div class="mt-1 text-xs text-fg-3">Tiempo de escucha esta semana</div>
</div>
<div>
<div class="font-display text-[25px] font-semibold tracking-[-0.03em] text-fg">
<div class="font-display text-2xl font-semibold tracking-[-0.03em] text-fg">
{stats.streakDays}
</div>
<div class="mt-1 text-[11.5px] text-fg-3">Días consecutivos escuchando</div>
<div class="mt-1 text-xs text-fg-3">Días consecutivos escuchando</div>
</div>
</div>
{/await}
@@ -220,7 +220,7 @@
oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(item))}
class="group/cont animate-enter flex items-center gap-3.25 rounded-[13px] p-2.25 pr-3.5 text-left transition-colors {isCurrent
? 'bg-accent-tint'
: 'hover:bg-white/[3%]'}"
: 'hover:bg-hover'}"
style="animation-delay:{Math.min(i, 10) * 40}ms"
>
<Cover
@@ -241,7 +241,7 @@
<ArtistLink
name={item.artist || '—'}
ownerUserId={item.ownerUserId}
class="mt-0.75 text-[12.5px] text-fg-3"
class="mt-0.75 text-xs text-fg-3"
/>
</div>
</div>
@@ -250,7 +250,7 @@
{:else}
<EmptyState
icon={History}
title="Aún no has escuchado nada"
title="Todavía no has escuchado nada"
description="Reproduce alguna canción y tu actividad reciente aparecerá aquí."
>
{#snippet actions()}
@@ -273,18 +273,18 @@
trackIds={spotlight.coverTrackIds}
version={spotlight.updatedAt}
size="large"
class="h-28 w-28 shrink-0 rounded-[16px] shadow-[0_16px_40px_rgba(0,0,0,.5)]"
class="h-28 w-28 shrink-0 rounded-[16px] shadow-cover-md"
/>
<div class="min-w-0">
<p class="text-[11px] font-semibold tracking-[0.16em] text-fg-2 uppercase">
<p class="text-xs font-semibold tracking-[0.16em] text-fg-2 uppercase">
Playlist destacada
</p>
<h3
class="mt-2.5 truncate font-display text-[28px] font-semibold tracking-[-0.03em] text-fg"
class="mt-2.5 truncate font-display text-2xl font-semibold tracking-[-0.03em] text-fg"
>
{spotlight.name}
</h3>
<p class="mt-2 text-[13px] text-fg-2">{spotlightMeta}</p>
<p class="mt-2 text-sm text-fg-2">{spotlightMeta}</p>
</div>
</div>
<p class="max-w-110 text-sm leading-[1.7] text-fg-2">
@@ -312,18 +312,16 @@
e.preventDefault();
playSpotlightTrack(i);
}}
class="flex items-center gap-3 rounded-[10px] p-2 text-left transition-colors hover:bg-white/[3%] {isTargetCurrent(
class="flex items-center gap-3 rounded-[10px] p-2 text-left transition-colors hover:bg-hover {isTargetCurrent(
target
)
? 'bg-accent-tint'
: ''}"
>
<span class="w-5 shrink-0 text-center text-[11.5px] text-muted tabular-nums"
>{i + 1}</span
>
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span>
<div class="min-w-0 flex-1">
<div
class="truncate text-[12.5px] font-medium {isTargetCurrent(target)
class="truncate text-sm font-medium {isTargetCurrent(target)
? 'text-accent-soft'
: 'text-fg'}"
>
@@ -332,10 +330,10 @@
<ArtistLink
name={targetArtist(target)}
ownerUserId={targetOwnerUserId(target)}
class="mt-0.5 text-[11px] text-fg-3"
class="mt-0.5 text-xs text-fg-3"
/>
</div>
<span class="shrink-0 text-[11.5px] text-muted tabular-nums">
<span class="shrink-0 text-xs text-muted tabular-nums">
{durationSeconds !== undefined ? fmtTime(durationSeconds) : '—'}
</span>
</div>
@@ -359,7 +357,7 @@
{:else}
<EmptyState
icon={Shuffle}
title="Aún no tienes mezclas"
title="Todavía no tienes mezclas"
description="Se generan automáticamente a partir de lo que escuchas."
/>
{/if}
@@ -387,7 +385,7 @@
oncontextmenu={(e) => openContextMenu(e, target)}
class="flex items-center gap-3.5 rounded-[11px] p-2 text-left transition-colors {isCurrent
? 'bg-accent-tint'
: 'hover:bg-white/[3%]'}"
: 'hover:bg-hover'}"
>
<span class="flex h-3.5 w-4.5 shrink-0 items-end justify-center">
{#if isCurrent}
@@ -409,9 +407,7 @@
<ExplicitBadge />
{/if}
<span
class="truncate text-[12.5px] font-medium {isCurrent
? 'text-accent-soft'
: 'text-fg'}"
class="truncate text-sm font-medium {isCurrent ? 'text-accent-soft' : 'text-fg'}"
>
{targetTitle(target)}
</span>
@@ -419,7 +415,7 @@
<ArtistLink
name={targetArtist(target)}
ownerUserId={targetOwnerUserId(target)}
class="mt-0.5 text-[11.5px] text-fg-3"
class="mt-0.5 text-xs text-fg-3"
/>
</div>
</div>
@@ -428,7 +424,7 @@
{:else}
<EmptyState
icon={History}
title="Aún no has escuchado nada"
title="Todavía no has escuchado nada"
description="Reproduce alguna canción y tu actividad reciente aparecerá aquí."
>
{#snippet actions()}
@@ -445,7 +441,7 @@
{#if data.playlistsHasMore}
<a
href="/playlists"
class="shrink-0 text-[11.5px] font-semibold tracking-[0.07em] text-fg-3 uppercase transition-colors hover:text-fg"
class="shrink-0 text-xs font-semibold tracking-[0.07em] text-fg-3 uppercase transition-colors hover:text-fg"
>
Ver todas
</a>
@@ -457,7 +453,7 @@
{#each playlists as playlist, i (playlist.id)}
<a
href="/playlists/{playlist.id}"
class="group/card animate-enter flex items-center gap-3.5 rounded-[15px] p-3 transition-colors hover:bg-white/[3%]"
class="group/card animate-enter flex items-center gap-3.5 rounded-[15px] p-3 transition-colors hover:bg-hover"
style="animation-delay:{Math.min(i, 10) * 40}ms"
>
<PlaylistArt
@@ -465,14 +461,14 @@
trackIds={playlist.coverTrackIds}
version={playlist.updatedAt}
size="medium"
class="h-15.5 w-15.5 shrink-0 rounded-[11px] shadow-[0_10px_24px_rgba(0,0,0,.42)]"
class="h-15.5 w-15.5 shrink-0 rounded-[11px] shadow-cover-sm"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-[13.5px] font-semibold tracking-[-0.01em] text-fg">
<div class="truncate text-sm font-semibold tracking-[-0.01em] text-fg">
{playlist.name}
</div>
{#if playlist.description}
<div class="mt-1 truncate text-[11.5px] text-fg-3">{playlist.description}</div>
<div class="mt-1 truncate text-xs text-fg-3">{playlist.description}</div>
{/if}
</div>
</a>
+6 -1
View File
@@ -9,7 +9,7 @@
--color-surface: var(--mf-surface);
--color-surface-2: var(--mf-surface-2);
--color-surface-hover: var(--mf-surface-hover);
--color-hover: rgba(255, 255, 255, 0.045);
--color-hover: var(--mf-surface-2);
--color-line: var(--mf-border);
--color-line-strong: var(--mf-border-strong);
--color-hairline: var(--mf-hairline);
@@ -48,6 +48,11 @@
--shadow-menu: 0 12px 28px -10px rgba(0, 0, 0, 0.6);
--shadow-player: 0 20px 56px rgba(0, 0, 0, 0.6);
--shadow-glow: 0 0 0 1px var(--mf-border-strong), 0 20px 45px -20px var(--mf-accent-glow);
--shadow-cover-lg: 0 24px 56px rgba(0, 0, 0, 0.55);
--shadow-cover-md: 0 16px 40px rgba(0, 0, 0, 0.5);
--shadow-cover-sm: 0 10px 24px rgba(0, 0, 0, 0.42);
--shadow-cover-xs: 0 8px 20px rgba(0, 0, 0, 0.5);
}
@utility page-x {