Crea src/lib/actions/pressable.ts y clickOutside.ts (Svelte actions; role/tabindex se mantienen estáticos en el markup por accesibilidad SSR, la acción solo añade click + Enter/Espacio) y src/lib/toggle.svelte.ts (createToggle), usado por playlists.svelte.ts y queuePanel.svelte.ts. format.ts: añade plural(n, uno, muchos); migra las 15 pluralizaciones manuales del repo, unificando a minúscula (Δ visible en Playlists, Inicio, liked, Explorar). player.svelte.ts: añade toQueueItem, isQueueCurrent, playAllOrToggle, playShuffled; renombra addToQueue → playNextItem (el nombre anterior confundía: inserta a continuación, no al final). Migra los 5 sitios que repetían el patrón isCurrentQueue/playAll (álbum, playlist, liked, mezcla, Inicio). Simplifica liked.svelte.ts y recentlyPlayed.ts con object spread en vez de reconstruir cada campo a mano. navigation.svelte.ts: añade searchHref (migra el buildHref duplicado de TopBar y Explorar) y appNavLinks (creado; su adopción en Sidebar/ TabsBar queda para P12, como indica esa parte). menus.svelte.ts: createTrackMenu() sustituye el estado manual de menú contextual (contextMenu + contextMenuStateFor + addToQueue) en Inicio, Explorar y Mezcla. Pendiente a propósito: la tarea 5 (eliminar el envoltorio TrackTarget de tracks.ts y sus 8 accesores) se difiere a P13 — es el cambio de mayor riesgo del plan, toca tipos de forma transversal en Inicio/ Explorar/Mezcla/menús, y no se pudo verificar visualmente sin Docker en esta sesión. Verificado con npm run check/build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
127 lines
3.4 KiB
Svelte
127 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { page } from '$app/state';
|
|
import type { SessionUser } from '$lib/types';
|
|
import { SEARCH_DEBOUNCE_MS, SEARCH_MIN_LENGTH } from '$lib/config';
|
|
import { searchHref } from '$lib/navigation.svelte';
|
|
import Search from '@lucide/svelte/icons/search';
|
|
import X from '@lucide/svelte/icons/x';
|
|
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
|
import AccountMenu from './ui/AccountMenu.svelte';
|
|
import Avatar from './ui/Avatar.svelte';
|
|
import IconButton from './ui/IconButton.svelte';
|
|
import Button from './ui/Button.svelte';
|
|
|
|
interface Props {
|
|
user: SessionUser | null;
|
|
accountUrl?: string | null;
|
|
}
|
|
|
|
let { user, accountUrl }: Props = $props();
|
|
|
|
let query = $derived(
|
|
page.url.pathname === '/explore' ? (page.url.searchParams.get('q') ?? '') : ''
|
|
);
|
|
let searchInput: HTMLInputElement | undefined = $state();
|
|
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
function runSearch(term: string) {
|
|
if (term.length > 0 && term.length < SEARCH_MIN_LENGTH) return;
|
|
goto(searchHref(term), {
|
|
replaceState: page.url.pathname === '/explore',
|
|
keepFocus: true,
|
|
noScroll: true
|
|
});
|
|
}
|
|
|
|
function onInput() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => runSearch(query.trim()), SEARCH_DEBOUNCE_MS);
|
|
}
|
|
|
|
function clearQuery() {
|
|
query = '';
|
|
clearTimeout(searchTimeout);
|
|
runSearch('');
|
|
searchInput?.focus();
|
|
}
|
|
|
|
function onSubmit(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
clearTimeout(searchTimeout);
|
|
runSearch(query.trim());
|
|
}
|
|
</script>
|
|
|
|
<header class="hidden items-center gap-3.5 px-6.5 pt-4 pb-3 sm:flex">
|
|
<IconButton
|
|
label="Atrás"
|
|
shape="round"
|
|
tone="subtle"
|
|
surface
|
|
size="xs"
|
|
onclick={() => history.back()}
|
|
>
|
|
<ChevronLeft class="size-icon-sm" strokeWidth={1.9} />
|
|
</IconButton>
|
|
|
|
<form onsubmit={onSubmit} class="max-w-search min-w-0 flex-1">
|
|
<label
|
|
class="flex h-9.5 w-full items-center gap-2.5 rounded-control bg-surface-2 px-3.5 transition focus-within:bg-surface-hover"
|
|
>
|
|
<Search class="h-3.75 w-3.75 shrink-0 text-fg-3" strokeWidth={1.8} />
|
|
<input
|
|
type="text"
|
|
bind:this={searchInput}
|
|
bind:value={query}
|
|
oninput={onInput}
|
|
placeholder="Canciones, artistas, álbumes o playlists"
|
|
class="w-full min-w-0 bg-transparent text-sm text-fg placeholder:text-muted focus:outline-none"
|
|
/>
|
|
{#if query}
|
|
<button
|
|
type="button"
|
|
onclick={clearQuery}
|
|
aria-label="Borrar búsqueda"
|
|
class="grid h-5 w-5 shrink-0 place-items-center rounded-full text-muted transition hover:text-fg"
|
|
>
|
|
<X class="h-3.5 w-3.5" />
|
|
</button>
|
|
{/if}
|
|
</label>
|
|
</form>
|
|
|
|
<div class="flex-1"></div>
|
|
|
|
<div class="relative shrink-0">
|
|
{#if user}
|
|
<AccountMenu
|
|
{user}
|
|
{accountUrl}
|
|
panelClass="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden"
|
|
>
|
|
{#snippet trigger({ toggle, open })}
|
|
<button
|
|
type="button"
|
|
onclick={toggle}
|
|
aria-label="Tu cuenta"
|
|
aria-expanded={open}
|
|
class="block shrink-0 rounded-full transition active:scale-95"
|
|
>
|
|
<Avatar
|
|
name={user.name}
|
|
src={user.picture}
|
|
size="sm"
|
|
class="border border-line-strong opacity-90"
|
|
/>
|
|
</button>
|
|
{/snippet}
|
|
</AccountMenu>
|
|
{:else}
|
|
<Button href="/auth/login" reload variant="accent" size="sm" class="shrink-0"
|
|
>Iniciar sesión</Button
|
|
>
|
|
{/if}
|
|
</div>
|
|
</header>
|