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
@@ -0,0 +1,73 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { SessionUser } from '$lib/types';
import LogOut from '@lucide/svelte/icons/log-out';
import User from '@lucide/svelte/icons/user';
import Settings from '@lucide/svelte/icons/settings';
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import MenuItem from './ui/MenuItem.svelte';
import GlassMenu from './ui/GlassMenu.svelte';
import { themeMode } from '$lib/theme/mode.svelte';
interface Props {
user: SessionUser;
accountUrl?: string | null;
panelClass?: string;
extraItems?: Snippet<[{ close: () => void }]>;
trigger: Snippet<[{ toggle: () => void; open: boolean }]>;
}
let {
user,
accountUrl,
panelClass = 'absolute top-full right-0 mt-2 w-52 overflow-hidden',
extraItems,
trigger
}: Props = $props();
let open = $state(false);
let menuRef: HTMLDivElement | undefined = $state();
function toggle() {
open = !open;
}
function close() {
open = false;
}
function onDocumentClick(event: MouseEvent) {
if (menuRef && !menuRef.contains(event.target as Node)) open = false;
}
</script>
<div class="relative" bind:this={menuRef}>
{@render trigger({ toggle, open })}
{#if open}
<GlassMenu class={panelClass}>
<MenuItem icon={User} label="Ver perfil" href="/u/{user.sub}" onclick={close} />
{@render extraItems?.({ close })}
{#if accountUrl}
<MenuItem
icon={Settings}
label="Ajustes"
href={accountUrl}
target="_blank"
onclick={close}
/>
{/if}
<MenuItem
icon={themeMode.current === 'dark' ? Sun : Moon}
label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'}
onclick={() => themeMode.toggle()}
/>
<form method="POST" action="/logout" data-sveltekit-reload>
<MenuItem icon={LogOut} label="Cerrar sesión" type="submit" />
</form>
</GlassMenu>
{/if}
</div>
<svelte:window onclick={onDocumentClick} />
@@ -60,7 +60,7 @@
<button
type="button"
onclick={onPlayNext}
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-[13.5px] font-medium text-fg-2 transition hover:bg-hover"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover"
>
<ListPlus class="h-[18px] w-[18px] shrink-0" strokeWidth={2} />
<span>Reproducir a continuación</span>
@@ -68,7 +68,7 @@
<button
type="button"
onclick={onAddToQueue}
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-[13.5px] font-medium text-fg-2 transition hover:bg-hover"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover"
>
<ListEnd class="h-[18px] w-[18px] shrink-0" strokeWidth={2} />
<span>Añadir a la cola</span>
@@ -76,7 +76,7 @@
{#if playlists.length > 0}
<div class="group/addmenu relative">
<div
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-[13.5px] font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
>
<span class="flex-1 cursor-default">Añadir álbum a una playlist</span>
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
@@ -106,7 +106,7 @@
<input type="hidden" name="albumId" value={menu.albumId} />
<button
type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-[13.5px] font-medium text-fg transition hover:bg-hover"
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"
>
<PlaylistArt
playlistId={playlist.id}
@@ -123,6 +123,6 @@
</div>
</div>
{:else}
<p class="px-2 py-2.5 text-sm text-fg-3">No tienes playlists todavía.</p>
<p class="px-2 py-2.5 text-sm text-fg-3">Todavía no tienes playlists.</p>
{/if}
</GlassMenu>
@@ -2,15 +2,9 @@
import type { SessionUser } from '$lib/types';
import Upload from '@lucide/svelte/icons/upload';
import Folder from '@lucide/svelte/icons/folder';
import LogOut from '@lucide/svelte/icons/log-out';
import LogIn from '@lucide/svelte/icons/log-in';
import User from '@lucide/svelte/icons/user';
import Settings from '@lucide/svelte/icons/settings';
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import MenuItem from './ui/MenuItem.svelte';
import GlassMenu from './ui/GlassMenu.svelte';
import { themeMode } from '$lib/theme/mode.svelte';
import AccountMenu from './AccountMenu.svelte';
interface Props {
user: SessionUser | null;
@@ -18,13 +12,6 @@
}
let { user, accountUrl }: Props = $props();
let menuOpen = $state(false);
let menuRef: HTMLDivElement | undefined = $state();
function onDocumentClick(event: MouseEvent) {
if (menuRef && !menuRef.contains(event.target as Node)) menuOpen = false;
}
</script>
<header
@@ -32,7 +19,7 @@
>
<a
href="/"
class="flex items-center gap-2 font-display text-[17.5px] font-semibold tracking-[-0.02em] text-fg"
class="flex items-center gap-2 font-display text-lg font-semibold tracking-[-0.02em] text-fg"
>
<span
class="h-5.5 w-5.5 rounded-[7px] bg-[image:var(--mf-logo-grad)] shadow-[var(--mf-logo-glow)]"
@@ -40,63 +27,37 @@
Musify
</a>
<div class="relative" bind:this={menuRef}>
<div class="relative">
{#if user}
<button
type="button"
onclick={() => (menuOpen = !menuOpen)}
aria-label="Tu cuenta"
aria-expanded={menuOpen}
class="flex items-center rounded-full"
<AccountMenu
{user}
{accountUrl}
panelClass="absolute top-full right-0 mt-2 w-56 overflow-hidden"
>
{#if user.picture}
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
{:else}
<span class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase">
{user.name.charAt(0)}
</span>
{/if}
</button>
{#if menuOpen}
<GlassMenu class="absolute top-full right-0 mt-2 w-56 overflow-hidden">
<MenuItem
icon={User}
label="Ver perfil"
href="/u/{user.sub}"
onclick={() => (menuOpen = false)}
/>
<MenuItem
icon={Folder}
label="Canciones subidas"
href="/library"
onclick={() => (menuOpen = false)}
/>
<MenuItem
icon={Upload}
label="Subir música"
href="/upload"
onclick={() => (menuOpen = false)}
/>
{#if accountUrl}
<MenuItem
icon={Settings}
label="Ajustes"
href={accountUrl}
target="_blank"
onclick={() => (menuOpen = false)}
/>
{/if}
<MenuItem
icon={themeMode.current === 'dark' ? Sun : Moon}
label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'}
onclick={() => themeMode.toggle()}
/>
<form method="POST" action="/logout" data-sveltekit-reload>
<MenuItem icon={LogOut} label="Cerrar sesión" type="submit" />
</form>
</GlassMenu>
{/if}
{#snippet trigger({ toggle, open })}
<button
type="button"
onclick={toggle}
aria-label="Tu cuenta"
aria-expanded={open}
class="flex items-center rounded-full"
>
{#if user.picture}
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
{:else}
<span
class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase"
>
{user.name.charAt(0)}
</span>
{/if}
</button>
{/snippet}
{#snippet extraItems({ close })}
<MenuItem icon={Folder} label="Canciones subidas" href="/library" onclick={close} />
<MenuItem icon={Upload} label="Subir música" href="/upload" onclick={close} />
{/snippet}
</AccountMenu>
{:else}
<a
href="/auth/login"
@@ -109,5 +70,3 @@
{/if}
</div>
</header>
<svelte:window onclick={onDocumentClick} />
+7 -8
View File
@@ -52,7 +52,7 @@
style="width:var(--mf-sidebar-w)"
>
<a href="/" class="flex items-center gap-2.5 px-1.5 pb-6.5">
<span class="font-display text-[17.5px] font-semibold tracking-[-0.02em] text-fg">Musify</span>
<span class="font-display text-lg font-semibold tracking-[-0.02em] text-fg">Musify</span>
</a>
<nav class="flex flex-col gap-0.5">
@@ -61,24 +61,23 @@
<a
href={link.href}
aria-current={active ? 'page' : undefined}
class="flex items-center gap-3 rounded-[10px] px-3 py-2.25 text-[13.5px] font-medium transition-colors duration-150 {active
class="flex items-center gap-3 rounded-[10px] px-3 py-2.25 text-sm font-medium transition-colors duration-150 {active
? 'bg-surface-hover text-fg'
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
>
<link.icon class="h-[17px] w-[17px] shrink-0" strokeWidth={1.6} />
<span class="flex-1 truncate">{link.label}</span>
{#if link.count}
<span class="text-[11px] text-muted tabular-nums">{link.count}</span>
<span class="text-xs text-muted tabular-nums">{link.count}</span>
{/if}
</a>
{/each}
</nav>
{#if user}
<div class="mx-3 mt-6 mb-3.5 h-px bg-white/5"></div>
<div class="mx-3 mt-6 mb-3.5 h-px bg-line"></div>
<div class="flex items-center justify-between px-3 pb-1.5">
<span class="text-[10.5px] font-semibold tracking-[0.13em] text-muted uppercase"
>Tus playlists</span
<span class="text-xs font-semibold tracking-[0.13em] text-muted uppercase">Tus playlists</span
>
{#if playlists.length > 0}
<button
@@ -109,7 +108,7 @@
class="h-8.5 w-8.5 shrink-0 overflow-hidden rounded-lg opacity-90"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-[13.5px] font-medium {active ? 'text-fg' : 'text-fg-2'}">
<div class="truncate text-sm font-medium {active ? 'text-fg' : 'text-fg-2'}">
{playlist.name}
</div>
</div>
@@ -118,7 +117,7 @@
<button
type="button"
onclick={() => createPlaylistModal.show()}
class="flex items-center gap-2 rounded-[10px] px-3 py-2 text-[12px] text-muted transition-colors hover:bg-hover hover:text-fg-2"
class="flex items-center gap-2 rounded-[10px] px-3 py-2 text-xs text-muted transition-colors hover:bg-hover hover:text-fg-2"
>
<Plus class="h-3.5 w-3.5" strokeWidth={1.8} />
Crear tu primera lista
+1 -1
View File
@@ -44,7 +44,7 @@
: 'text-fg-2'}"
>
<link.icon class="h-[17px] w-[17px]" strokeWidth={1.6} />
<span class="text-[10px] font-medium">{link.label}</span>
<span class="text-xs font-medium">{link.label}</span>
</a>
{/each}
</nav>
+28 -62
View File
@@ -7,14 +7,7 @@
import X from '@lucide/svelte/icons/x';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import LogOut from '@lucide/svelte/icons/log-out';
import User from '@lucide/svelte/icons/user';
import Settings from '@lucide/svelte/icons/settings';
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import MenuItem from './ui/MenuItem.svelte';
import GlassMenu from './ui/GlassMenu.svelte';
import { themeMode } from '$lib/theme/mode.svelte';
import AccountMenu from './AccountMenu.svelte';
interface Props {
user: SessionUser | null;
@@ -26,8 +19,6 @@
let query = $derived(
page.url.pathname === '/explore' ? (page.url.searchParams.get('q') ?? '') : ''
);
let menuOpen = $state(false);
let menuRef: HTMLDivElement | undefined = $state();
let searchInput: HTMLInputElement | undefined = $state();
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -61,10 +52,6 @@
clearTimeout(searchTimeout);
runSearch(query.trim());
}
function onDocumentClick(event: MouseEvent) {
if (menuRef && !menuRef.contains(event.target as Node)) menuOpen = false;
}
</script>
<header class="hidden items-center gap-3.5 px-6.5 pt-4 pb-3 sm:flex">
@@ -73,13 +60,13 @@
type="button"
onclick={() => history.back()}
aria-label="Atrás"
class="grid h-7.5 w-7.5 place-items-center rounded-full bg-white/[3.5%] text-fg-2 transition-colors hover:bg-white/7 hover:text-fg"
class="grid h-7.5 w-7.5 place-items-center rounded-full bg-surface-2 text-fg-2 transition-colors hover:bg-surface-hover hover:text-fg"
>
<ChevronLeft class="h-[15px] w-[15px]" strokeWidth={1.9} />
</button>
<span
aria-hidden="true"
class="grid h-7.5 w-7.5 place-items-center rounded-full bg-white/[3.5%] text-muted"
class="grid h-7.5 w-7.5 place-items-center rounded-full bg-surface-2 text-muted"
>
<ChevronRight class="h-[15px] w-[15px]" strokeWidth={1.9} />
</span>
@@ -87,7 +74,7 @@
<form onsubmit={onSubmit} class="max-w-[480px] min-w-0 flex-1">
<label
class="flex h-9.5 w-full items-center gap-2.5 rounded-[11px] bg-white/[3.5%] px-3.5 transition focus-within:bg-white/[6%]"
class="flex h-9.5 w-full items-center gap-2.5 rounded-[11px] bg-surface-2 px-3.5 transition focus-within:bg-surface-hover"
>
<Search class="h-[15px] w-[15px] shrink-0 text-fg-3" strokeWidth={1.8} />
<input
@@ -96,7 +83,7 @@
bind:value={query}
oninput={onInput}
placeholder="Canciones, artistas, álbumes o playlists"
class="w-full min-w-0 bg-transparent text-[13px] text-fg placeholder:text-muted focus:outline-none"
class="w-full min-w-0 bg-transparent text-sm text-fg placeholder:text-muted focus:outline-none"
/>
{#if query}
<button
@@ -113,51 +100,32 @@
<div class="flex-1"></div>
<div class="relative shrink-0" bind:this={menuRef}>
<div class="relative shrink-0">
{#if user}
<button
type="button"
onclick={() => (menuOpen = !menuOpen)}
aria-label="Tu cuenta"
class="block h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-white/10 transition active:scale-95"
<AccountMenu
{user}
{accountUrl}
panelClass="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden"
>
{#if user.picture}
<img src={user.picture} alt="" class="h-full w-full object-cover opacity-90" />
{:else}
<span
class="grid h-full w-full place-items-center bg-surface-2 text-xs font-semibold text-fg uppercase"
{#snippet trigger({ toggle })}
<button
type="button"
onclick={toggle}
aria-label="Tu cuenta"
class="block h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-line-strong transition active:scale-95"
>
{user.name.charAt(0)}
</span>
{/if}
</button>
{#if menuOpen}
<GlassMenu class="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden">
<MenuItem
icon={User}
label="Ver perfil"
href="/u/{user.sub}"
onclick={() => (menuOpen = false)}
/>
{#if accountUrl}
<MenuItem
icon={Settings}
label="Ajustes"
href={accountUrl}
target="_blank"
onclick={() => (menuOpen = false)}
/>
{/if}
<MenuItem
icon={themeMode.current === 'dark' ? Sun : Moon}
label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'}
onclick={() => themeMode.toggle()}
/>
<form method="POST" action="/logout" data-sveltekit-reload>
<MenuItem icon={LogOut} label="Cerrar sesión" type="submit" />
</form>
</GlassMenu>
{/if}
{#if user.picture}
<img src={user.picture} alt="" class="h-full w-full object-cover opacity-90" />
{:else}
<span
class="grid h-full w-full place-items-center bg-surface-2 text-xs font-semibold text-fg uppercase"
>
{user.name.charAt(0)}
</span>
{/if}
</button>
{/snippet}
</AccountMenu>
{:else}
<a
href="/auth/login"
@@ -169,5 +137,3 @@
{/if}
</div>
</header>
<svelte:window onclick={onDocumentClick} />
@@ -53,7 +53,7 @@
<button
type="button"
onclick={onAddToQueue}
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-[13.5px] font-medium text-fg-2 transition hover:bg-hover"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover"
>
<ListPlus class="h-[18px] w-[18px] shrink-0" strokeWidth={2} />
<span>Reproducir a continuación</span>
@@ -61,7 +61,7 @@
{#if playlists.length > 0}
<div class="group/addmenu relative">
<div
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-[13.5px] font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
>
<span class="flex-1 cursor-default">Añadir a una playlist</span>
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
@@ -97,7 +97,7 @@
{/if}
<button
type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-[13.5px] font-medium text-fg transition hover:bg-hover"
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"
>
<PlaylistArt
playlistId={playlist.id}
@@ -21,7 +21,7 @@
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
>
<div class="mb-5 flex items-center justify-between">
<h2 class="font-display text-[14.5px] font-semibold tracking-[-0.02em] text-fg">En cola</h2>
<h2 class="font-display text-base font-semibold tracking-[-0.02em] text-fg">En cola</h2>
<button
type="button"
onclick={() => queuePanel.close()}
@@ -33,7 +33,7 @@
</div>
{#if player.current.id}
<p class="mb-2.75 text-[10.5px] font-semibold tracking-[0.12em] text-muted uppercase">
<p class="mb-2.75 text-xs font-semibold tracking-[0.12em] text-muted uppercase">
Reproduciendo
</p>
<div class="mb-6 flex items-center gap-3 rounded-xl bg-accent-tint p-2.5">
@@ -45,13 +45,13 @@
class="h-10 w-10 shrink-0 rounded-[9px]"
/>
<div class="min-w-0">
<div class="truncate text-[12.5px] font-semibold text-accent-soft">
<div class="truncate text-sm font-semibold text-accent-soft">
{player.current.title}
</div>
<ArtistLink
name={player.current.artist || '—'}
ownerUserId={player.current.ownerUserId}
class="mt-0.75 text-[11px] text-fg-3"
class="mt-0.75 text-xs text-fg-3"
/>
</div>
</div>
@@ -59,7 +59,7 @@
<div class="mb-2.75 flex items-center justify-between gap-3">
<span
class="min-w-0 flex-1 truncate text-[10.5px] font-semibold tracking-[0.12em] text-muted uppercase"
class="min-w-0 flex-1 truncate text-xs font-semibold tracking-[0.12em] text-muted uppercase"
>
A continuación
</span>
@@ -67,7 +67,7 @@
<button
type="button"
onclick={() => player.clearUpcoming()}
class="shrink-0 text-[11px] text-muted transition-colors hover:text-fg"
class="shrink-0 text-xs text-muted transition-colors hover:text-fg"
>
Vaciar
</button>
@@ -96,15 +96,15 @@
class="h-9 w-9 shrink-0 rounded-lg opacity-90"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-[12.5px] font-medium text-fg">{track.title}</div>
<div class="truncate text-sm font-medium text-fg">{track.title}</div>
<ArtistLink
name={track.artist || '—'}
ownerUserId={track.ownerUserId}
class="mt-0.5 text-[11px] text-fg-3"
class="mt-0.5 text-xs text-fg-3"
/>
</div>
{#if track.duration}
<span class="shrink-0 text-[11px] text-muted tabular-nums"
<span class="shrink-0 text-xs text-muted tabular-nums"
>{fmtTime(track.duration)}</span
>
{/if}
@@ -28,7 +28,7 @@
</button>
{:else}
<div class="flex w-full max-w-135 items-center gap-2.75">
<span class="min-w-8 text-right text-[10.5px] text-fg-3 tabular-nums">
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
{fmtTime(player.progress)}
</span>
<button
@@ -41,7 +41,7 @@
<div class="h-full rounded-full bg-accent" style="width:{player.progressPercent}%"></div>
</div>
</button>
<span class="min-w-8 text-[10.5px] text-fg-3 tabular-nums">
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
{fmtTime(player.current.duration)}
</span>
</div>
@@ -35,26 +35,24 @@
src={player.current.coverUrl}
size="small"
alt={player.current.title}
class="h-11 w-11 shrink-0 rounded-[11px] shadow-[0_8px_20px_rgba(0,0,0,0.5)] md:h-12 md:w-12"
class="h-11 w-11 shrink-0 rounded-[11px] shadow-cover-xs md:h-12 md:w-12"
/>
{:else}
<div
class="h-11 w-11 shrink-0 rounded-[11px] bg-surface shadow-[0_8px_20px_rgba(0,0,0,0.5)] md:h-12 md:w-12"
></div>
<div class="h-11 w-11 shrink-0 rounded-[11px] bg-surface shadow-cover-xs md:h-12 md:w-12"></div>
{/if}
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-1.5">
{#if player.current.explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-[13px] font-semibold tracking-[-0.01em] text-fg"
<span class="truncate text-sm font-semibold tracking-[-0.01em] text-fg"
>{player.current.title}</span
>
</div>
<ArtistLink
name={player.current.artist || '—'}
ownerUserId={player.current.ownerUserId}
class="mt-0.5 text-[11.5px] text-fg-3"
class="mt-0.5 text-xs text-fg-3"
/>
</div>
{#if !compact}
@@ -27,9 +27,9 @@
{:else}
<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)"
style="background:repeating-linear-gradient(135deg, color-mix(in oklch, var(--mf-text) 12%, transparent) 0 2px, transparent 2px 10px)"
></div>
<span class="relative text-lg font-semibold text-white/90">{initials}</span>
<span class="relative text-lg font-semibold text-fg">{initials}</span>
{/if}
</div>
<div class="w-full truncate text-sm text-fg">{name}</div>
@@ -29,12 +29,12 @@
}: Props = $props();
const sizes = {
sm: 'px-4 py-2.5 text-[12.5px]',
md: 'px-5 py-3 text-[13px]',
sm: 'px-4 py-2.5 text-xs',
md: 'px-5 py-3 text-sm',
lg: 'px-6 py-3.5 text-sm'
};
const subtleButton = 'bg-white/[6%] text-fg-2 hover:bg-white/[9%] hover:text-fg';
const subtleButton = 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg';
const variants = {
primary: subtleButton,
@@ -0,0 +1,43 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
eyebrow: string;
title: string;
description?: string;
meta?: string;
align?: 'center' | 'end';
cover: Snippet;
actions: Snippet;
}
let { eyebrow, title, description, meta, align = 'end', cover, actions }: Props = $props();
</script>
<div
class="flex flex-col gap-5 sm:flex-row sm:gap-6 {align === 'end'
? 'mt-3 sm:mt-4 sm:items-end'
: 'sm:items-center'}"
>
{@render cover()}
<div class="min-w-0 flex-1">
<p class="text-xs font-semibold tracking-[0.16em] text-fg-2 uppercase">{eyebrow}</p>
<h1
class="{align === 'end'
? 'mt-1.5'
: 'mt-3'} font-display text-3xl font-semibold tracking-[-0.035em] break-words text-fg sm:text-4xl"
>
{title}
</h1>
{#if description}
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
{/if}
{#if meta}
<p class="mt-3 text-sm text-muted">{meta}</p>
{/if}
</div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
{@render actions()}
</div>
@@ -23,7 +23,7 @@
</h3>
{/if}
{#if description}
<p class="mt-2.5 max-w-sm text-[13px] leading-[1.65] text-fg-2">{description}</p>
<p class="mt-2.5 max-w-sm text-sm leading-[1.65] text-fg-2">{description}</p>
{/if}
{#if actions}
<div class="mt-6 flex flex-wrap items-center justify-center gap-2.5">{@render actions()}</div>
@@ -7,7 +7,7 @@
</script>
<span
class="grid h-4 w-4 shrink-0 place-items-center rounded-sm bg-fg-3 text-[11px] leading-4 font-bold text-bg {klass}"
class="grid h-4 w-4 shrink-0 place-items-center rounded-sm bg-fg-3 text-xs leading-4 font-bold text-bg {klass}"
title="Contenido explícito"
aria-label="Contenido explícito">E</span
>
@@ -57,6 +57,6 @@
class="h-full w-full object-cover"
/>
{:else}
<Icon class="h-9 w-9 {gradient ? 'text-white/70' : 'text-muted'}" strokeWidth={1.3} />
<Icon class="h-9 w-9 {gradient ? 'text-on-art/70' : 'text-muted'}" strokeWidth={1.3} />
{/if}
</label>
@@ -27,6 +27,6 @@
{@render art(mediaCardArt)}
<div class="mt-2.5 truncate text-sm font-medium text-fg">{title}</div>
{#if subtitle}
<div class="mt-0.5 truncate text-[12.5px] text-fg-3">{subtitle}</div>
<div class="mt-0.5 truncate text-xs text-fg-3">{subtitle}</div>
{/if}
</a>
@@ -24,7 +24,7 @@
}: Props = $props();
const classes = $derived(
`flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-[13.5px] font-medium text-fg-2 transition hover:bg-hover ${klass}`
`flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg-2 transition hover:bg-hover ${klass}`
);
</script>
@@ -37,15 +37,15 @@
type="button"
onclick={play}
aria-label="Reproducir {mix.title}"
class="absolute right-2.5 bottom-2.5 grid h-9.5 w-9.5 place-items-center rounded-full border border-white/12 bg-ink/76 text-fg opacity-0 backdrop-blur-[8px] transition-opacity duration-150 group-hover/tile:opacity-100"
class="absolute right-2.5 bottom-2.5 grid h-9.5 w-9.5 place-items-center rounded-full border border-on-art/12 bg-ink/76 text-fg opacity-0 backdrop-blur-[8px] transition-opacity duration-150 group-hover/tile:opacity-100"
>
<Play class="h-3.5 w-3.5" fill="currentColor" />
</button>
</div>
<div class="mt-3.25 truncate text-[13px] font-semibold tracking-[-0.01em] text-fg">
<div class="mt-3.25 truncate text-sm font-semibold tracking-[-0.01em] text-fg">
{mix.title}
</div>
{#if mix.subtitle}
<div class="mt-1 truncate text-[11.5px] text-fg-3">{mix.subtitle}</div>
<div class="mt-1 truncate text-xs text-fg-3">{mix.subtitle}</div>
{/if}
</a>
@@ -13,11 +13,11 @@
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}">
<div class="min-w-0">
<h1 class="font-display text-[28px] font-semibold tracking-[-0.03em] text-fg sm:text-[32px]">
<h1 class="font-display text-3xl font-semibold tracking-[-0.03em] text-fg sm:text-4xl">
{title}
</h1>
{#if subtitle}
<p class="mt-2 text-[13px] text-fg-2">{subtitle}</p>
<p class="mt-2 text-sm text-fg-2">{subtitle}</p>
{/if}
</div>
{#if actions}
@@ -49,14 +49,14 @@
{#if explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-[13px] font-medium text-fg">{title}</span>
<span class="truncate text-sm font-medium text-fg">{title}</span>
</div>
{#if subtitle}
<div class="mt-0.75 truncate text-[11.5px] text-fg-3">{subtitle}</div>
<div class="mt-0.75 truncate text-xs text-fg-3">{subtitle}</div>
{/if}
</div>
{#if meta}
<span class="shrink-0 text-[11.5px] text-muted tabular-nums">{meta}</span>
<span class="shrink-0 text-xs text-muted tabular-nums">{meta}</span>
{/if}
{/snippet}
@@ -12,7 +12,7 @@
</script>
{#snippet heading()}
<h2 class="font-display text-[17.5px] font-medium tracking-[-0.025em] text-fg">
<h2 class="font-display text-lg font-medium tracking-[-0.025em] text-fg">
{title}
</h2>
{#if subtitle}
@@ -9,6 +9,6 @@
let { class: klass = '', children }: Props = $props();
</script>
<span class="truncate text-center text-[12.5px] text-muted tabular-nums {klass}">
<span class="truncate text-center text-xs text-muted tabular-nums {klass}">
{@render children()}
</span>
@@ -19,7 +19,7 @@
<div
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
? 'bg-accent-tint'
: 'hover:bg-white/[3.2%]'} {klass}"
: 'hover:bg-hover'} {klass}"
style="--mf-track-cols:{table.columns}; --mf-track-cols-mobile:{table.columnsMobile}"
{oncontextmenu}
>
@@ -56,7 +56,7 @@
<div class="overflow-x-auto {klass}">
<div
class="grid track-grid items-center gap-3 border-b border-white/5 pr-4 pb-3 pl-2 text-[10.5px] font-semibold tracking-[0.12em] text-muted uppercase sm:gap-8 sm:pr-6 sm:pl-3"
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-xs font-semibold tracking-[0.12em] text-muted uppercase sm:gap-8 sm:pr-6 sm:pl-3"
style="--mf-track-cols:{columns}; --mf-track-cols-mobile:{columnsMobile}"
>
{#if index}
@@ -69,6 +69,6 @@
>
{@render badge?.()}
</div>
<ArtistLink name={artist} {ownerUserId} class="mt-0.5 text-[12.5px] text-fg-3" />
<ArtistLink name={artist} {ownerUserId} class="mt-0.5 text-xs text-fg-3" />
</div>
</div>