Design Prot Merge #1
@@ -6,6 +6,30 @@
|
|||||||
> No es necesario ejecutar todo de una vez: está pensado para trabajarse página por
|
> No es necesario ejecutar todo de una vez: está pensado para trabajarse página por
|
||||||
> página o componente por componente, en PRs pequeños y verificables.
|
> página o componente por componente, en PRs pequeños y verificables.
|
||||||
|
|
||||||
|
> **Estado (sesión de seguimiento posterior):** §1–§3 y §5 de este documento (hex
|
||||||
|
> literales, tamaños de fuente arbitrarios, menú "de cristal" duplicado, componentes
|
||||||
|
> huérfanos listados) ya están resueltos. El problema había reaparecido con otra forma:
|
||||||
|
> valores de opacidad sueltos sobre `white`/`black` (`bg-white/[6%]`, `border-white/10`,
|
||||||
|
> etc.) fuera de los tokens `--mf-surface*`/`--mf-border*`, sombras/gradientes de portada
|
||||||
|
> duplicados sin token, un bloque de "cabecera de colección" reimplementado 5 veces, un
|
||||||
|
> menú de cuenta duplicado en `TopBar`/`MobileHeader`, y textos de estado vacío con
|
||||||
|
> redacción inconsistente ("Aún no..." vs "Todavía no..."). Todo eso se corrigió: se
|
||||||
|
> añadieron tokens `--shadow-cover-lg/md/sm/xs` y `--mf-liked-grad` a
|
||||||
|
> `layout.css`/`theme.css`, se migraron los literales `white`/`black` a
|
||||||
|
> `bg-surface*`/`border-line*`/`hover:bg-hover`/`text-on-art` según si el color está
|
||||||
|
> sobre el "chrome" de la app o sobre arte/gradiente saturado, se extrajeron
|
||||||
|
> `lib/components/ui/CollectionHeader.svelte` (portada + título + meta + acciones, usado
|
||||||
|
> en `AlbumHeader`, `PlaylistHeader`, `liked/+page.svelte`, `mixes/[id]/+page.svelte`,
|
||||||
|
> `albums/external/.../+page.svelte`) y `lib/components/AccountMenu.svelte` (estado de
|
||||||
|
> apertura + panel del menú de cuenta, usado en `TopBar`/`MobileHeader`), y se corrigió
|
||||||
|
> `--color-hover` en `layout.css`, que estaba hardcodeado en vez de apuntar a
|
||||||
|
> `--mf-surface-2` (rompía el modo claro para cualquier `hover:bg-hover`). Pendiente de
|
||||||
|
> quien retome esto: verificación visual en el navegador con una sesión autenticada real
|
||||||
|
> (esta sesión no tenía credenciales del IdP de Zitadel ni acceso para activar
|
||||||
|
> `allowAnonymousListening`), y `lib/components/ui/Rail.svelte`, que el grep de
|
||||||
|
> huérfanos de §5 marca sin importadores — no se tocó porque no forma parte de este
|
||||||
|
> barrido, pero conviene confirmarlo a mano y borrarlo si procede.
|
||||||
|
|
||||||
## 0. Objetivo
|
## 0. Objetivo
|
||||||
|
|
||||||
Tres problemas, en orden de aparición histórica pero **no** de prioridad recomendada:
|
Tres problemas, en orden de aparición histórica pero **no** de prioridad recomendada:
|
||||||
|
|||||||
@@ -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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={onPlayNext}
|
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} />
|
<ListPlus class="h-[18px] w-[18px] shrink-0" strokeWidth={2} />
|
||||||
<span>Reproducir a continuación</span>
|
<span>Reproducir a continuación</span>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={onAddToQueue}
|
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} />
|
<ListEnd class="h-[18px] w-[18px] shrink-0" strokeWidth={2} />
|
||||||
<span>Añadir a la cola</span>
|
<span>Añadir a la cola</span>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
{#if playlists.length > 0}
|
{#if playlists.length > 0}
|
||||||
<div class="group/addmenu relative">
|
<div class="group/addmenu relative">
|
||||||
<div
|
<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>
|
<span class="flex-1 cursor-default">Añadir álbum a una playlist</span>
|
||||||
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
|
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<input type="hidden" name="albumId" value={menu.albumId} />
|
<input type="hidden" name="albumId" value={menu.albumId} />
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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
|
<PlaylistArt
|
||||||
playlistId={playlist.id}
|
playlistId={playlist.id}
|
||||||
@@ -123,6 +123,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</GlassMenu>
|
</GlassMenu>
|
||||||
|
|||||||
@@ -2,15 +2,9 @@
|
|||||||
import type { SessionUser } from '$lib/types';
|
import type { SessionUser } from '$lib/types';
|
||||||
import Upload from '@lucide/svelte/icons/upload';
|
import Upload from '@lucide/svelte/icons/upload';
|
||||||
import Folder from '@lucide/svelte/icons/folder';
|
import Folder from '@lucide/svelte/icons/folder';
|
||||||
import LogOut from '@lucide/svelte/icons/log-out';
|
|
||||||
import LogIn from '@lucide/svelte/icons/log-in';
|
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 MenuItem from './ui/MenuItem.svelte';
|
||||||
import GlassMenu from './ui/GlassMenu.svelte';
|
import AccountMenu from './AccountMenu.svelte';
|
||||||
import { themeMode } from '$lib/theme/mode.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: SessionUser | null;
|
user: SessionUser | null;
|
||||||
@@ -18,13 +12,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { user, accountUrl }: Props = $props();
|
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>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
@@ -32,7 +19,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/"
|
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
|
<span
|
||||||
class="h-5.5 w-5.5 rounded-[7px] bg-[image:var(--mf-logo-grad)] shadow-[var(--mf-logo-glow)]"
|
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
|
Musify
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="relative" bind:this={menuRef}>
|
<div class="relative">
|
||||||
{#if user}
|
{#if user}
|
||||||
<button
|
<AccountMenu
|
||||||
type="button"
|
{user}
|
||||||
onclick={() => (menuOpen = !menuOpen)}
|
{accountUrl}
|
||||||
aria-label="Tu cuenta"
|
panelClass="absolute top-full right-0 mt-2 w-56 overflow-hidden"
|
||||||
aria-expanded={menuOpen}
|
|
||||||
class="flex items-center rounded-full"
|
|
||||||
>
|
>
|
||||||
{#if user.picture}
|
{#snippet trigger({ toggle, open })}
|
||||||
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
|
<button
|
||||||
{:else}
|
type="button"
|
||||||
<span class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase">
|
onclick={toggle}
|
||||||
{user.name.charAt(0)}
|
aria-label="Tu cuenta"
|
||||||
</span>
|
aria-expanded={open}
|
||||||
{/if}
|
class="flex items-center rounded-full"
|
||||||
</button>
|
>
|
||||||
|
{#if user.picture}
|
||||||
{#if menuOpen}
|
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
|
||||||
<GlassMenu class="absolute top-full right-0 mt-2 w-56 overflow-hidden">
|
{:else}
|
||||||
<MenuItem
|
<span
|
||||||
icon={User}
|
class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase"
|
||||||
label="Ver perfil"
|
>
|
||||||
href="/u/{user.sub}"
|
{user.name.charAt(0)}
|
||||||
onclick={() => (menuOpen = false)}
|
</span>
|
||||||
/>
|
{/if}
|
||||||
<MenuItem
|
</button>
|
||||||
icon={Folder}
|
{/snippet}
|
||||||
label="Canciones subidas"
|
{#snippet extraItems({ close })}
|
||||||
href="/library"
|
<MenuItem icon={Folder} label="Canciones subidas" href="/library" onclick={close} />
|
||||||
onclick={() => (menuOpen = false)}
|
<MenuItem icon={Upload} label="Subir música" href="/upload" onclick={close} />
|
||||||
/>
|
{/snippet}
|
||||||
<MenuItem
|
</AccountMenu>
|
||||||
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}
|
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="/auth/login"
|
href="/auth/login"
|
||||||
@@ -109,5 +70,3 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<svelte:window onclick={onDocumentClick} />
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
style="width:var(--mf-sidebar-w)"
|
style="width:var(--mf-sidebar-w)"
|
||||||
>
|
>
|
||||||
<a href="/" class="flex items-center gap-2.5 px-1.5 pb-6.5">
|
<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>
|
</a>
|
||||||
|
|
||||||
<nav class="flex flex-col gap-0.5">
|
<nav class="flex flex-col gap-0.5">
|
||||||
@@ -61,24 +61,23 @@
|
|||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
aria-current={active ? 'page' : undefined}
|
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'
|
? 'bg-surface-hover text-fg'
|
||||||
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
|
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
|
||||||
>
|
>
|
||||||
<link.icon class="h-[17px] w-[17px] shrink-0" strokeWidth={1.6} />
|
<link.icon class="h-[17px] w-[17px] shrink-0" strokeWidth={1.6} />
|
||||||
<span class="flex-1 truncate">{link.label}</span>
|
<span class="flex-1 truncate">{link.label}</span>
|
||||||
{#if link.count}
|
{#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}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{#if user}
|
{#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">
|
<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"
|
<span class="text-xs font-semibold tracking-[0.13em] text-muted uppercase">Tus playlists</span
|
||||||
>Tus playlists</span
|
|
||||||
>
|
>
|
||||||
{#if playlists.length > 0}
|
{#if playlists.length > 0}
|
||||||
<button
|
<button
|
||||||
@@ -109,7 +108,7 @@
|
|||||||
class="h-8.5 w-8.5 shrink-0 overflow-hidden rounded-lg opacity-90"
|
class="h-8.5 w-8.5 shrink-0 overflow-hidden rounded-lg opacity-90"
|
||||||
/>
|
/>
|
||||||
<div class="min-w-0 flex-1">
|
<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}
|
{playlist.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +117,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => createPlaylistModal.show()}
|
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} />
|
<Plus class="h-3.5 w-3.5" strokeWidth={1.8} />
|
||||||
Crear tu primera lista
|
Crear tu primera lista
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
: 'text-fg-2'}"
|
: 'text-fg-2'}"
|
||||||
>
|
>
|
||||||
<link.icon class="h-[17px] w-[17px]" strokeWidth={1.6} />
|
<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>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -7,14 +7,7 @@
|
|||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||||
import LogOut from '@lucide/svelte/icons/log-out';
|
import AccountMenu from './AccountMenu.svelte';
|
||||||
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 {
|
interface Props {
|
||||||
user: SessionUser | null;
|
user: SessionUser | null;
|
||||||
@@ -26,8 +19,6 @@
|
|||||||
let query = $derived(
|
let query = $derived(
|
||||||
page.url.pathname === '/explore' ? (page.url.searchParams.get('q') ?? '') : ''
|
page.url.pathname === '/explore' ? (page.url.searchParams.get('q') ?? '') : ''
|
||||||
);
|
);
|
||||||
let menuOpen = $state(false);
|
|
||||||
let menuRef: HTMLDivElement | undefined = $state();
|
|
||||||
let searchInput: HTMLInputElement | undefined = $state();
|
let searchInput: HTMLInputElement | undefined = $state();
|
||||||
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
@@ -61,10 +52,6 @@
|
|||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
runSearch(query.trim());
|
runSearch(query.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDocumentClick(event: MouseEvent) {
|
|
||||||
if (menuRef && !menuRef.contains(event.target as Node)) menuOpen = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="hidden items-center gap-3.5 px-6.5 pt-4 pb-3 sm:flex">
|
<header class="hidden items-center gap-3.5 px-6.5 pt-4 pb-3 sm:flex">
|
||||||
@@ -73,13 +60,13 @@
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={() => history.back()}
|
onclick={() => history.back()}
|
||||||
aria-label="Atrás"
|
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} />
|
<ChevronLeft class="h-[15px] w-[15px]" strokeWidth={1.9} />
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
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} />
|
<ChevronRight class="h-[15px] w-[15px]" strokeWidth={1.9} />
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +74,7 @@
|
|||||||
|
|
||||||
<form onsubmit={onSubmit} class="max-w-[480px] min-w-0 flex-1">
|
<form onsubmit={onSubmit} class="max-w-[480px] min-w-0 flex-1">
|
||||||
<label
|
<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} />
|
<Search class="h-[15px] w-[15px] shrink-0 text-fg-3" strokeWidth={1.8} />
|
||||||
<input
|
<input
|
||||||
@@ -96,7 +83,7 @@
|
|||||||
bind:value={query}
|
bind:value={query}
|
||||||
oninput={onInput}
|
oninput={onInput}
|
||||||
placeholder="Canciones, artistas, álbumes o playlists"
|
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}
|
{#if query}
|
||||||
<button
|
<button
|
||||||
@@ -113,51 +100,32 @@
|
|||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
<div class="relative shrink-0" bind:this={menuRef}>
|
<div class="relative shrink-0">
|
||||||
{#if user}
|
{#if user}
|
||||||
<button
|
<AccountMenu
|
||||||
type="button"
|
{user}
|
||||||
onclick={() => (menuOpen = !menuOpen)}
|
{accountUrl}
|
||||||
aria-label="Tu cuenta"
|
panelClass="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden"
|
||||||
class="block h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-white/10 transition active:scale-95"
|
|
||||||
>
|
>
|
||||||
{#if user.picture}
|
{#snippet trigger({ toggle })}
|
||||||
<img src={user.picture} alt="" class="h-full w-full object-cover opacity-90" />
|
<button
|
||||||
{:else}
|
type="button"
|
||||||
<span
|
onclick={toggle}
|
||||||
class="grid h-full w-full place-items-center bg-surface-2 text-xs font-semibold text-fg uppercase"
|
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)}
|
{#if user.picture}
|
||||||
</span>
|
<img src={user.picture} alt="" class="h-full w-full object-cover opacity-90" />
|
||||||
{/if}
|
{:else}
|
||||||
</button>
|
<span
|
||||||
{#if menuOpen}
|
class="grid h-full w-full place-items-center bg-surface-2 text-xs font-semibold text-fg uppercase"
|
||||||
<GlassMenu class="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden">
|
>
|
||||||
<MenuItem
|
{user.name.charAt(0)}
|
||||||
icon={User}
|
</span>
|
||||||
label="Ver perfil"
|
{/if}
|
||||||
href="/u/{user.sub}"
|
</button>
|
||||||
onclick={() => (menuOpen = false)}
|
{/snippet}
|
||||||
/>
|
</AccountMenu>
|
||||||
{#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}
|
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="/auth/login"
|
href="/auth/login"
|
||||||
@@ -169,5 +137,3 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<svelte:window onclick={onDocumentClick} />
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={onAddToQueue}
|
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} />
|
<ListPlus class="h-[18px] w-[18px] shrink-0" strokeWidth={2} />
|
||||||
<span>Reproducir a continuación</span>
|
<span>Reproducir a continuación</span>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{#if playlists.length > 0}
|
{#if playlists.length > 0}
|
||||||
<div class="group/addmenu relative">
|
<div class="group/addmenu relative">
|
||||||
<div
|
<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>
|
<span class="flex-1 cursor-default">Añadir a una playlist</span>
|
||||||
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
|
<ChevronRight class="h-4 w-4 shrink-0 text-fg-3" />
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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
|
<PlaylistArt
|
||||||
playlistId={playlist.id}
|
playlistId={playlist.id}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
|
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => queuePanel.close()}
|
onclick={() => queuePanel.close()}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if player.current.id}
|
{#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
|
Reproduciendo
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-6 flex items-center gap-3 rounded-xl bg-accent-tint p-2.5">
|
<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]"
|
class="h-10 w-10 shrink-0 rounded-[9px]"
|
||||||
/>
|
/>
|
||||||
<div class="min-w-0">
|
<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}
|
{player.current.title}
|
||||||
</div>
|
</div>
|
||||||
<ArtistLink
|
<ArtistLink
|
||||||
name={player.current.artist || '—'}
|
name={player.current.artist || '—'}
|
||||||
ownerUserId={player.current.ownerUserId}
|
ownerUserId={player.current.ownerUserId}
|
||||||
class="mt-0.75 text-[11px] text-fg-3"
|
class="mt-0.75 text-xs text-fg-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
<div class="mb-2.75 flex items-center justify-between gap-3">
|
<div class="mb-2.75 flex items-center justify-between gap-3">
|
||||||
<span
|
<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
|
A continuación
|
||||||
</span>
|
</span>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => player.clearUpcoming()}
|
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
|
Vaciar
|
||||||
</button>
|
</button>
|
||||||
@@ -96,15 +96,15 @@
|
|||||||
class="h-9 w-9 shrink-0 rounded-lg opacity-90"
|
class="h-9 w-9 shrink-0 rounded-lg opacity-90"
|
||||||
/>
|
/>
|
||||||
<div class="min-w-0 flex-1">
|
<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
|
<ArtistLink
|
||||||
name={track.artist || '—'}
|
name={track.artist || '—'}
|
||||||
ownerUserId={track.ownerUserId}
|
ownerUserId={track.ownerUserId}
|
||||||
class="mt-0.5 text-[11px] text-fg-3"
|
class="mt-0.5 text-xs text-fg-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if track.duration}
|
{#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
|
>{fmtTime(track.duration)}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex w-full max-w-135 items-center gap-2.75">
|
<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)}
|
{fmtTime(player.progress)}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<div class="h-full rounded-full bg-accent" style="width:{player.progressPercent}%"></div>
|
<div class="h-full rounded-full bg-accent" style="width:{player.progressPercent}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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)}
|
{fmtTime(player.current.duration)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,26 +35,24 @@
|
|||||||
src={player.current.coverUrl}
|
src={player.current.coverUrl}
|
||||||
size="small"
|
size="small"
|
||||||
alt={player.current.title}
|
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}
|
{:else}
|
||||||
<div
|
<div class="h-11 w-11 shrink-0 rounded-[11px] bg-surface shadow-cover-xs md:h-12 md:w-12"></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>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex min-w-0 items-center gap-1.5">
|
<div class="flex min-w-0 items-center gap-1.5">
|
||||||
{#if player.current.explicit}
|
{#if player.current.explicit}
|
||||||
<ExplicitBadge />
|
<ExplicitBadge />
|
||||||
{/if}
|
{/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
|
>{player.current.title}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<ArtistLink
|
<ArtistLink
|
||||||
name={player.current.artist || '—'}
|
name={player.current.artist || '—'}
|
||||||
ownerUserId={player.current.ownerUserId}
|
ownerUserId={player.current.ownerUserId}
|
||||||
class="mt-0.5 text-[11.5px] text-fg-3"
|
class="mt-0.5 text-xs text-fg-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !compact}
|
{#if !compact}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0"
|
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>
|
></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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full truncate text-sm text-fg">{name}</div>
|
<div class="w-full truncate text-sm text-fg">{name}</div>
|
||||||
|
|||||||
@@ -29,12 +29,12 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: 'px-4 py-2.5 text-[12.5px]',
|
sm: 'px-4 py-2.5 text-xs',
|
||||||
md: 'px-5 py-3 text-[13px]',
|
md: 'px-5 py-3 text-sm',
|
||||||
lg: 'px-6 py-3.5 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 = {
|
const variants = {
|
||||||
primary: subtleButton,
|
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>
|
</h3>
|
||||||
{/if}
|
{/if}
|
||||||
{#if description}
|
{#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}
|
||||||
{#if actions}
|
{#if actions}
|
||||||
<div class="mt-6 flex flex-wrap items-center justify-center gap-2.5">{@render actions()}</div>
|
<div class="mt-6 flex flex-wrap items-center justify-center gap-2.5">{@render actions()}</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<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"
|
title="Contenido explícito"
|
||||||
aria-label="Contenido explícito">E</span
|
aria-label="Contenido explícito">E</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -57,6 +57,6 @@
|
|||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -27,6 +27,6 @@
|
|||||||
{@render art(mediaCardArt)}
|
{@render art(mediaCardArt)}
|
||||||
<div class="mt-2.5 truncate text-sm font-medium text-fg">{title}</div>
|
<div class="mt-2.5 truncate text-sm font-medium text-fg">{title}</div>
|
||||||
{#if subtitle}
|
{#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}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const classes = $derived(
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -37,15 +37,15 @@
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={play}
|
onclick={play}
|
||||||
aria-label="Reproducir {mix.title}"
|
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" />
|
<Play class="h-3.5 w-3.5" fill="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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}
|
{mix.title}
|
||||||
</div>
|
</div>
|
||||||
{#if mix.subtitle}
|
{#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}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}">
|
||||||
<div class="min-w-0">
|
<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}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
{#if subtitle}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if actions}
|
{#if actions}
|
||||||
|
|||||||
@@ -49,14 +49,14 @@
|
|||||||
{#if explicit}
|
{#if explicit}
|
||||||
<ExplicitBadge />
|
<ExplicitBadge />
|
||||||
{/if}
|
{/if}
|
||||||
<span class="truncate text-[13px] font-medium text-fg">{title}</span>
|
<span class="truncate text-sm font-medium text-fg">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if subtitle}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if meta}
|
{#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}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet heading()}
|
{#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}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{#if subtitle}
|
{#if subtitle}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
let { class: klass = '', children }: Props = $props();
|
let { class: klass = '', children }: Props = $props();
|
||||||
</script>
|
</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()}
|
{@render children()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div
|
<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
|
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'
|
? '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}"
|
style="--mf-track-cols:{table.columns}; --mf-track-cols-mobile:{table.columnsMobile}"
|
||||||
{oncontextmenu}
|
{oncontextmenu}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<div class="overflow-x-auto {klass}">
|
<div class="overflow-x-auto {klass}">
|
||||||
<div
|
<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}"
|
style="--mf-track-cols:{columns}; --mf-track-cols-mobile:{columnsMobile}"
|
||||||
>
|
>
|
||||||
{#if index}
|
{#if index}
|
||||||
|
|||||||
@@ -69,6 +69,6 @@
|
|||||||
>
|
>
|
||||||
{@render badge?.()}
|
{@render badge?.()}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
--mf-logo-grad: linear-gradient(140deg, oklch(0.78 0.15 145), oklch(0.62 0.13 185) 92%);
|
--mf-logo-grad: linear-gradient(140deg, oklch(0.78 0.15 145), oklch(0.62 0.13 185) 92%);
|
||||||
--mf-logo-glow: 0 0 20px oklch(0.74 0.145 145 / 0.3);
|
--mf-logo-glow: 0 0 20px oklch(0.74 0.145 145 / 0.3);
|
||||||
--mf-cover-grad: linear-gradient(150deg, oklch(0.5 0.13 145), oklch(0.16 0.035 145));
|
--mf-cover-grad: linear-gradient(150deg, oklch(0.5 0.13 145), oklch(0.16 0.035 145));
|
||||||
|
--mf-liked-grad: linear-gradient(150deg, oklch(0.4 0.09 330), oklch(0.18 0.04 10));
|
||||||
|
|
||||||
--mf-accent: oklch(0.74 0.145 145);
|
--mf-accent: oklch(0.74 0.145 145);
|
||||||
--mf-accent-title: oklch(0.87 0.07 145);
|
--mf-accent-title: oklch(0.87 0.07 145);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||||
import BackLink from '$lib/components/ui/BackLink.svelte';
|
import BackLink from '$lib/components/ui/BackLink.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -48,45 +49,34 @@
|
|||||||
label={isOwner ? 'Volver a tus álbumes' : 'Volver al inicio'}
|
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">
|
<CollectionHeader eyebrow="Álbum" {title} {description} {meta}>
|
||||||
<PlaylistArt
|
{#snippet cover()}
|
||||||
{trackIds}
|
<PlaylistArt
|
||||||
{coverUrl}
|
{trackIds}
|
||||||
size="large"
|
{coverUrl}
|
||||||
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
|
size="large"
|
||||||
/>
|
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg 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>
|
{/snippet}
|
||||||
<h1
|
{#snippet actions()}
|
||||||
class="mt-1.5 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
|
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
|
||||||
>
|
{#if playing}
|
||||||
{title}
|
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
||||||
</h1>
|
Pausar
|
||||||
{#if description}
|
{:else}
|
||||||
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
|
<Play class="h-4 w-4" strokeWidth={1.5} />
|
||||||
{/if}
|
Reproducir
|
||||||
<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">
|
|
||||||
<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
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={onDelete}>
|
{#if isOwner}
|
||||||
<Trash class="h-4 w-4" strokeWidth={1.5} />
|
<Button size="sm" variant="secondary" onclick={onEdit}>
|
||||||
Eliminar
|
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
|
||||||
</Button>
|
Editar
|
||||||
{/if}
|
</Button>
|
||||||
</div>
|
<Button size="sm" variant="secondary" onclick={onDelete}>
|
||||||
|
<Trash class="h-4 w-4" strokeWidth={1.5} />
|
||||||
|
Eliminar
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</CollectionHeader>
|
||||||
|
|||||||
+35
-42
@@ -8,6 +8,7 @@
|
|||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Cover from '$lib/components/ui/Cover.svelte';
|
import Cover from '$lib/components/ui/Cover.svelte';
|
||||||
import Alert from '$lib/components/ui/Alert.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 TrackTable from '$lib/components/ui/TrackTable.svelte';
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
||||||
@@ -79,48 +80,40 @@
|
|||||||
<Page>
|
<Page>
|
||||||
<BackLink href="/explore" label="Volver a explorar" />
|
<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">
|
<CollectionHeader
|
||||||
<Cover
|
eyebrow={album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'}
|
||||||
trackId={album.albumId}
|
title={album.title}
|
||||||
src={album.thumbnailUrl}
|
description={album.artist}
|
||||||
size="large"
|
meta={[
|
||||||
alt={album.title}
|
album.releaseYear ? String(album.releaseYear) : undefined,
|
||||||
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
|
`${tracks.length} ${tracks.length === 1 ? 'canción' : 'canciones'}`,
|
||||||
oncontextmenu={(e) => (albumMenu = albumContextMenuStateFor(e, 'youtube', album.albumId))}
|
fmtTime(Number(data.album.totalDurationSeconds))
|
||||||
/>
|
]
|
||||||
<div class="min-w-0 flex-1">
|
.filter((part) => !!part)
|
||||||
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">
|
.join(' · ')}
|
||||||
{album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'}
|
>
|
||||||
</p>
|
{#snippet cover()}
|
||||||
<h1
|
<Cover
|
||||||
class="mt-1.5 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
|
trackId={album.albumId}
|
||||||
>
|
src={album.thumbnailUrl}
|
||||||
{album.title}
|
size="large"
|
||||||
</h1>
|
alt={album.title}
|
||||||
<p class="mt-3 text-base text-fg-2">{album.artist}</p>
|
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
|
||||||
<p class="mt-1 text-sm text-muted">
|
oncontextmenu={(e) => (albumMenu = albumContextMenuStateFor(e, 'youtube', album.albumId))}
|
||||||
{[
|
/>
|
||||||
album.releaseYear ? String(album.releaseYear) : undefined,
|
{/snippet}
|
||||||
`${tracks.length} ${tracks.length === 1 ? 'canción' : 'canciones'}`,
|
{#snippet actions()}
|
||||||
fmtTime(Number(data.album.totalDurationSeconds))
|
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
|
||||||
]
|
{#if isCurrentQueue && player.playing}
|
||||||
.filter((part) => !!part)
|
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
||||||
.join(' · ')}
|
Pausar
|
||||||
</p>
|
{:else}
|
||||||
</div>
|
<Play class="h-4 w-4" strokeWidth={1.5} />
|
||||||
</div>
|
Reproducir
|
||||||
|
{/if}
|
||||||
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
|
</Button>
|
||||||
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
|
{/snippet}
|
||||||
{#if isCurrentQueue && player.playing}
|
</CollectionHeader>
|
||||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
|
||||||
Pausar
|
|
||||||
{:else}
|
|
||||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
|
||||||
Reproducir
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if form?.message}
|
{#if form?.message}
|
||||||
<Alert tone="danger" class="mt-4">{form.message}</Alert>
|
<Alert tone="danger" class="mt-4">{form.message}</Alert>
|
||||||
|
|||||||
@@ -313,7 +313,7 @@
|
|||||||
Resultados de búsqueda
|
Resultados de búsqueda
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<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}»
|
Resultados para «{data.query}»
|
||||||
</h1>
|
</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 ===
|
class="flex items-center gap-1.75 rounded-full px-3.5 py-1.75 text-xs font-medium transition {sfilter ===
|
||||||
chip.label
|
chip.label
|
||||||
? 'bg-cta-strong text-ink'
|
? '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>{chip.label}</span>
|
||||||
<span
|
<span class="text-xs tabular-nums {sfilter === chip.label ? 'opacity-55' : 'text-muted'}"
|
||||||
class="text-[10.5px] tabular-nums {sfilter === chip.label
|
>{chip.count}</span
|
||||||
? 'opacity-55'
|
|
||||||
: 'text-muted'}">{chip.count}</span
|
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -394,9 +392,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
||||||
<div
|
<div class="text-xs leading-none font-semibold tracking-[0.14em] text-fg-3 uppercase">
|
||||||
class="text-xs leading-none font-semibold tracking-[0.14em] text-fg-3 uppercase"
|
|
||||||
>
|
|
||||||
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
|
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -456,7 +452,7 @@
|
|||||||
<h1 class="font-display text-xl font-semibold tracking-[-0.025em] text-fg">
|
<h1 class="font-display text-xl font-semibold tracking-[-0.025em] text-fg">
|
||||||
Sin resultados para «{data.query}»
|
Sin resultados para «{data.query}»
|
||||||
</h1>
|
</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
|
Revisa la ortografía o prueba con un término más corto. También puedes buscar por álbum o
|
||||||
por playlist.
|
por playlist.
|
||||||
</p>
|
</p>
|
||||||
@@ -464,7 +460,7 @@
|
|||||||
{#each genreTiles.slice(0, 4) as genre (genre.query)}
|
{#each genreTiles.slice(0, 4) as genre (genre.query)}
|
||||||
<a
|
<a
|
||||||
href={buildHref(genre.query)}
|
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}
|
{genre.label}
|
||||||
</a>
|
</a>
|
||||||
@@ -481,12 +477,12 @@
|
|||||||
href={buildHref(genre.query)}
|
href={buildHref(genre.query)}
|
||||||
class="animate-enter group relative block h-28 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-125"
|
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) *
|
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}
|
{genre.label}
|
||||||
</div>
|
</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>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -500,7 +496,7 @@
|
|||||||
<SectionHeading title="Canciones">
|
<SectionHeading title="Canciones">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<div class="h-px flex-1 self-center bg-line"></div>
|
<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}
|
||||||
{searchCounts.Canciones === 1 ? 'Resultado' : 'Resultados'}
|
{searchCounts.Canciones === 1 ? 'Resultado' : 'Resultados'}
|
||||||
</span>
|
</span>
|
||||||
@@ -553,7 +549,7 @@
|
|||||||
<SectionHeading title="Álbumes">
|
<SectionHeading title="Álbumes">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<div class="h-px flex-1 self-center bg-line"></div>
|
<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}
|
||||||
{searchCounts.Álbumes === 1 ? 'Resultado' : 'Resultados'}
|
{searchCounts.Álbumes === 1 ? 'Resultado' : 'Resultados'}
|
||||||
</span>
|
</span>
|
||||||
@@ -605,7 +601,7 @@
|
|||||||
<SectionHeading title="Usuarios">
|
<SectionHeading title="Usuarios">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<div class="h-px flex-1 self-center bg-line"></div>
|
<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}
|
||||||
{searchCounts.Usuarios === 1 ? 'Resultado' : 'Resultados'}
|
{searchCounts.Usuarios === 1 ? 'Resultado' : 'Resultados'}
|
||||||
</span>
|
</span>
|
||||||
@@ -638,7 +634,7 @@
|
|||||||
<SectionHeading title="Playlists">
|
<SectionHeading title="Playlists">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<div class="h-px flex-1 self-center bg-line"></div>
|
<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}
|
||||||
{searchCounts.Playlists === 1 ? 'Resultado' : 'Resultados'}
|
{searchCounts.Playlists === 1 ? 'Resultado' : 'Resultados'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { liked } from '$lib/player/liked.svelte';
|
import { liked } from '$lib/player/liked.svelte';
|
||||||
import { fmtDate } from '$lib/format';
|
import { fmtDate } from '$lib/format';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
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 Button from '$lib/components/ui/Button.svelte';
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
@@ -46,38 +47,31 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6">
|
<CollectionHeader
|
||||||
<div
|
eyebrow="Colección"
|
||||||
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"
|
title="Me gusta"
|
||||||
style="background:linear-gradient(150deg, oklch(0.4 0.09 330), oklch(0.18 0.04 10))"
|
meta="{tracks.length} {tracks.length === 1 ? 'canción' : 'canciones'}"
|
||||||
>
|
align="center"
|
||||||
<Heart class="h-10 w-10 text-white/90" fill="currentColor" strokeWidth={0} />
|
>
|
||||||
</div>
|
{#snippet cover()}
|
||||||
<div class="min-w-0 flex-1">
|
<div
|
||||||
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Colección</p>
|
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"
|
||||||
<h1
|
|
||||||
class="mt-3 font-display text-[28px] font-semibold tracking-[-0.035em] text-fg sm:text-[33px]"
|
|
||||||
>
|
>
|
||||||
Me gusta
|
<Heart class="h-10 w-10 text-on-art/90" fill="currentColor" strokeWidth={0} />
|
||||||
</h1>
|
</div>
|
||||||
<p class="mt-3.25 text-[12.5px] text-fg-2">
|
{/snippet}
|
||||||
{tracks.length}
|
{#snippet actions()}
|
||||||
{tracks.length === 1 ? 'canción' : 'canciones'}
|
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
||||||
</p>
|
{#if isCurrentQueue && player.playing}
|
||||||
</div>
|
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
||||||
</div>
|
Pausar
|
||||||
|
{:else}
|
||||||
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
|
<Play class="h-4 w-4" strokeWidth={1.5} />
|
||||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
Reproducir
|
||||||
{#if isCurrentQueue && player.playing}
|
{/if}
|
||||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
</Button>
|
||||||
Pausar
|
{/snippet}
|
||||||
{:else}
|
</CollectionHeader>
|
||||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
|
||||||
Reproducir
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if tracks.length > 0}
|
{#if tracks.length > 0}
|
||||||
<TrackTable
|
<TrackTable
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Alert from '$lib/components/ui/Alert.svelte';
|
import Alert from '$lib/components/ui/Alert.svelte';
|
||||||
import MixArt from '$lib/components/ui/MixArt.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 TrackTable from '$lib/components/ui/TrackTable.svelte';
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
||||||
@@ -67,43 +68,31 @@
|
|||||||
<Page>
|
<Page>
|
||||||
<BackLink href="/" label="Volver al inicio" />
|
<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">
|
<CollectionHeader
|
||||||
<MixArt
|
eyebrow="Mezcla"
|
||||||
{items}
|
title={mix.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"
|
description={mix.subtitle ?? undefined}
|
||||||
/>
|
meta="{items.length} {items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}"
|
||||||
<div class="min-w-0 flex-1">
|
>
|
||||||
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Mezcla</p>
|
{#snippet cover()}
|
||||||
<h1
|
<MixArt {items} class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41" />
|
||||||
class="mt-1.5 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
|
{/snippet}
|
||||||
>
|
{#snippet actions()}
|
||||||
{mix.title}
|
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
|
||||||
</h1>
|
{#if isCurrentQueue && player.playing}
|
||||||
{#if mix.subtitle}
|
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
||||||
<p class="mt-3 text-base text-fg-2">{mix.subtitle}</p>
|
Pausar
|
||||||
{/if}
|
{:else}
|
||||||
<p class="mt-1 text-sm text-muted">
|
<Play class="h-4 w-4" strokeWidth={1.5} />
|
||||||
{items.length}
|
Reproducir
|
||||||
{items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}
|
{/if}
|
||||||
</p>
|
</Button>
|
||||||
</div>
|
<Button variant="secondary" size="sm" onclick={playShuffled} disabled={queue.length === 0}>
|
||||||
</div>
|
<Shuffle class="h-4 w-4" />
|
||||||
|
Aleatorio
|
||||||
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
|
</Button>
|
||||||
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
|
{/snippet}
|
||||||
{#if isCurrentQueue && player.playing}
|
</CollectionHeader>
|
||||||
<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>
|
|
||||||
|
|
||||||
{#if form?.message}
|
{#if form?.message}
|
||||||
<Alert tone="danger" class="mt-4">{form.message}</Alert>
|
<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">
|
<h2 class="font-display text-xl font-semibold tracking-[-0.02em] text-fg">
|
||||||
¿Eliminar «{playlist.name}»?
|
¿Eliminar «{playlist.name}»?
|
||||||
</h2>
|
</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
|
Esta acción no se puede deshacer, la playlist se eliminará de tu biblioteca y de todos los
|
||||||
dispositivos donde la tengas guardada.
|
dispositivos donde la tengas guardada.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<script lang="ts">
|
<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 PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,47 +28,39 @@
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onDelete
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
const meta = $derived(
|
||||||
|
`${trackIds.length} ${trackIds.length === 1 ? 'canción' : 'canciones'} · ${
|
||||||
|
visibility === 'Public' ? 'Pública' : 'Privada'
|
||||||
|
}`
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6">
|
<CollectionHeader
|
||||||
<PlaylistArt
|
eyebrow="Lista"
|
||||||
playlistId={id}
|
title={name}
|
||||||
{trackIds}
|
description={description ?? undefined}
|
||||||
size="large"
|
{meta}
|
||||||
version={updatedAt}
|
align="center"
|
||||||
class="h-36 w-36 shrink-0 rounded-[18px] shadow-[0_24px_56px_rgba(0,0,0,.55)] sm:h-41 sm:w-41"
|
>
|
||||||
/>
|
{#snippet cover()}
|
||||||
<div class="min-w-0 flex-1">
|
<PlaylistArt
|
||||||
<p class="text-[10.5px] font-semibold tracking-[0.16em] text-fg-2 uppercase">Lista</p>
|
playlistId={id}
|
||||||
<h1
|
{trackIds}
|
||||||
class="mt-3 font-display text-[28px] font-semibold tracking-[-0.035em] break-words text-fg sm:text-[33px]"
|
size="large"
|
||||||
>
|
version={updatedAt}
|
||||||
{name}
|
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
|
||||||
</h1>
|
/>
|
||||||
{#if description}
|
{/snippet}
|
||||||
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
|
{#snippet actions()}
|
||||||
{/if}
|
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
|
||||||
<p class="mt-3.25 text-[12.5px] text-fg-2">
|
{#if playing}
|
||||||
{trackIds.length}
|
Pausar
|
||||||
{trackIds.length === 1 ? 'canción' : 'canciones'} · {visibility === 'Public'
|
{:else}
|
||||||
? 'Pública'
|
Reproducir
|
||||||
: 'Privada'}
|
{/if}
|
||||||
</p>
|
</Button>
|
||||||
</div>
|
<Button size="sm" variant="secondary" onclick={onEdit}>Editar</Button>
|
||||||
</div>
|
<Button size="sm" variant="secondary" onclick={onDelete}>Eliminar</Button>
|
||||||
|
{/snippet}
|
||||||
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
|
</CollectionHeader>
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6">
|
<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} />
|
<ArtistAvatar name={profile.name} imageUrl={profile.profilePictureUrl} size={144} />
|
||||||
<div class="min-w-0 flex-1">
|
<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
|
<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}
|
{profile.name}
|
||||||
</h1>
|
</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}
|
||||||
{profile.followersCount === 1 ? 'seguidor' : 'seguidores'} · {profile.followingCount} siguiendo
|
{profile.followersCount === 1 ? 'seguidor' : 'seguidores'} · {profile.followingCount} siguiendo
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
class="grid h-7 w-7 shrink-0 place-items-center rounded-full text-xs font-bold {stepStates[
|
class="grid h-7 w-7 shrink-0 place-items-center rounded-full text-xs font-bold {stepStates[
|
||||||
i
|
i
|
||||||
] === 'idle'
|
] === 'idle'
|
||||||
? 'bg-white/[3.5%] text-fg-2'
|
? 'bg-surface-2 text-fg-2'
|
||||||
: 'bg-accent-btn text-accent-soft'}"
|
: 'bg-accent-btn text-accent-soft'}"
|
||||||
>
|
>
|
||||||
{#if stepStates[i] === 'done'}
|
{#if stepStates[i] === 'done'}
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
{#if coverPreview !== ''}
|
{#if coverPreview !== ''}
|
||||||
<img src={coverPreview} alt="Portada" class="h-full w-full object-cover" />
|
<img src={coverPreview} alt="Portada" class="h-full w-full object-cover" />
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<h1 class="mb-2.5 font-display text-xl font-semibold tracking-[-0.02em] text-fg">
|
<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'}
|
{page.status === 404 ? 'Página no encontrada' : 'No se pudo cargar tu música'}
|
||||||
</h1>
|
</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
|
{page.status === 404
|
||||||
? 'Comprueba el enlace o vuelve al inicio.'
|
? 'Comprueba el enlace o vuelve al inicio.'
|
||||||
: 'La conexión se interrumpió. Lo que tengas descargado seguirá disponible sin conexión.'}
|
: 'La conexión se interrumpió. Lo que tengas descargado seguirá disponible sin conexión.'}
|
||||||
|
|||||||
@@ -152,11 +152,11 @@
|
|||||||
>
|
>
|
||||||
<div class="animate-enter max-w-135 min-w-65">
|
<div class="animate-enter max-w-135 min-w-65">
|
||||||
<h1
|
<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}
|
{data.greeting}
|
||||||
</h1>
|
</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.
|
Sube lo tuyo, descubre lo nuevo y escúchalo todo en un solo sitio.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6.5 flex flex-wrap items-center gap-2.5">
|
<div class="mt-6.5 flex flex-wrap items-center gap-2.5">
|
||||||
@@ -177,22 +177,22 @@
|
|||||||
{#await data.listeningStats then stats}
|
{#await data.listeningStats then stats}
|
||||||
<div class="flex gap-6.5">
|
<div class="flex gap-6.5">
|
||||||
<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.tracksThisWeek}
|
{stats.tracksThisWeek}
|
||||||
</div>
|
</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>
|
<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))}
|
{fmtDurationLong(Number(stats.secondsThisWeek))}
|
||||||
</div>
|
</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>
|
<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}
|
{stats.streakDays}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(item))}
|
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
|
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'
|
? 'bg-accent-tint'
|
||||||
: 'hover:bg-white/[3%]'}"
|
: 'hover:bg-hover'}"
|
||||||
style="animation-delay:{Math.min(i, 10) * 40}ms"
|
style="animation-delay:{Math.min(i, 10) * 40}ms"
|
||||||
>
|
>
|
||||||
<Cover
|
<Cover
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
<ArtistLink
|
<ArtistLink
|
||||||
name={item.artist || '—'}
|
name={item.artist || '—'}
|
||||||
ownerUserId={item.ownerUserId}
|
ownerUserId={item.ownerUserId}
|
||||||
class="mt-0.75 text-[12.5px] text-fg-3"
|
class="mt-0.75 text-xs text-fg-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={History}
|
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í."
|
description="Reproduce alguna canción y tu actividad reciente aparecerá aquí."
|
||||||
>
|
>
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
@@ -273,18 +273,18 @@
|
|||||||
trackIds={spotlight.coverTrackIds}
|
trackIds={spotlight.coverTrackIds}
|
||||||
version={spotlight.updatedAt}
|
version={spotlight.updatedAt}
|
||||||
size="large"
|
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">
|
<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
|
Playlist destacada
|
||||||
</p>
|
</p>
|
||||||
<h3
|
<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}
|
{spotlight.name}
|
||||||
</h3>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<p class="max-w-110 text-sm leading-[1.7] text-fg-2">
|
<p class="max-w-110 text-sm leading-[1.7] text-fg-2">
|
||||||
@@ -312,18 +312,16 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
playSpotlightTrack(i);
|
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
|
target
|
||||||
)
|
)
|
||||||
? 'bg-accent-tint'
|
? 'bg-accent-tint'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<span class="w-5 shrink-0 text-center text-[11.5px] text-muted tabular-nums"
|
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span>
|
||||||
>{i + 1}</span
|
|
||||||
>
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div
|
<div
|
||||||
class="truncate text-[12.5px] font-medium {isTargetCurrent(target)
|
class="truncate text-sm font-medium {isTargetCurrent(target)
|
||||||
? 'text-accent-soft'
|
? 'text-accent-soft'
|
||||||
: 'text-fg'}"
|
: 'text-fg'}"
|
||||||
>
|
>
|
||||||
@@ -332,10 +330,10 @@
|
|||||||
<ArtistLink
|
<ArtistLink
|
||||||
name={targetArtist(target)}
|
name={targetArtist(target)}
|
||||||
ownerUserId={targetOwnerUserId(target)}
|
ownerUserId={targetOwnerUserId(target)}
|
||||||
class="mt-0.5 text-[11px] text-fg-3"
|
class="mt-0.5 text-xs text-fg-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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) : '—'}
|
{durationSeconds !== undefined ? fmtTime(durationSeconds) : '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,7 +357,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Shuffle}
|
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."
|
description="Se generan automáticamente a partir de lo que escuchas."
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -387,7 +385,7 @@
|
|||||||
oncontextmenu={(e) => openContextMenu(e, target)}
|
oncontextmenu={(e) => openContextMenu(e, target)}
|
||||||
class="flex items-center gap-3.5 rounded-[11px] p-2 text-left transition-colors {isCurrent
|
class="flex items-center gap-3.5 rounded-[11px] p-2 text-left transition-colors {isCurrent
|
||||||
? 'bg-accent-tint'
|
? '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">
|
<span class="flex h-3.5 w-4.5 shrink-0 items-end justify-center">
|
||||||
{#if isCurrent}
|
{#if isCurrent}
|
||||||
@@ -409,9 +407,7 @@
|
|||||||
<ExplicitBadge />
|
<ExplicitBadge />
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="truncate text-[12.5px] font-medium {isCurrent
|
class="truncate text-sm font-medium {isCurrent ? 'text-accent-soft' : 'text-fg'}"
|
||||||
? 'text-accent-soft'
|
|
||||||
: 'text-fg'}"
|
|
||||||
>
|
>
|
||||||
{targetTitle(target)}
|
{targetTitle(target)}
|
||||||
</span>
|
</span>
|
||||||
@@ -419,7 +415,7 @@
|
|||||||
<ArtistLink
|
<ArtistLink
|
||||||
name={targetArtist(target)}
|
name={targetArtist(target)}
|
||||||
ownerUserId={targetOwnerUserId(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>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,7 +424,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={History}
|
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í."
|
description="Reproduce alguna canción y tu actividad reciente aparecerá aquí."
|
||||||
>
|
>
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
@@ -445,7 +441,7 @@
|
|||||||
{#if data.playlistsHasMore}
|
{#if data.playlistsHasMore}
|
||||||
<a
|
<a
|
||||||
href="/playlists"
|
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
|
Ver todas
|
||||||
</a>
|
</a>
|
||||||
@@ -457,7 +453,7 @@
|
|||||||
{#each playlists as playlist, i (playlist.id)}
|
{#each playlists as playlist, i (playlist.id)}
|
||||||
<a
|
<a
|
||||||
href="/playlists/{playlist.id}"
|
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"
|
style="animation-delay:{Math.min(i, 10) * 40}ms"
|
||||||
>
|
>
|
||||||
<PlaylistArt
|
<PlaylistArt
|
||||||
@@ -465,14 +461,14 @@
|
|||||||
trackIds={playlist.coverTrackIds}
|
trackIds={playlist.coverTrackIds}
|
||||||
version={playlist.updatedAt}
|
version={playlist.updatedAt}
|
||||||
size="medium"
|
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="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}
|
{playlist.name}
|
||||||
</div>
|
</div>
|
||||||
{#if playlist.description}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
--color-surface: var(--mf-surface);
|
--color-surface: var(--mf-surface);
|
||||||
--color-surface-2: var(--mf-surface-2);
|
--color-surface-2: var(--mf-surface-2);
|
||||||
--color-surface-hover: var(--mf-surface-hover);
|
--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: var(--mf-border);
|
||||||
--color-line-strong: var(--mf-border-strong);
|
--color-line-strong: var(--mf-border-strong);
|
||||||
--color-hairline: var(--mf-hairline);
|
--color-hairline: var(--mf-hairline);
|
||||||
@@ -48,6 +48,11 @@
|
|||||||
--shadow-menu: 0 12px 28px -10px rgba(0, 0, 0, 0.6);
|
--shadow-menu: 0 12px 28px -10px rgba(0, 0, 0, 0.6);
|
||||||
--shadow-player: 0 20px 56px 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-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 {
|
@utility page-x {
|
||||||
|
|||||||
Reference in New Issue
Block a user