Files
Musify/web-player/src/lib/components/TopBar.svelte
T
Admin 5696683859 Fix light-mode regression and finish design-system cleanup
A code-review pass on the previous commit found a real bug: the explore
genre tiles used var(--mf-ink) as a decorative gradient endpoint, but that
token is meant for text-on-accent and flips to white in light mode,
turning the tiles solid white. Replaces it with a new, deliberately
theme-invariant --mf-tile-shade token (same category as --mf-cover-grad).

Also: raises CollectionHeader's meta-line contrast in light mode
(text-muted, ~3:1, to text-fg-2), makes its actions prop optional so
pages without conditional actions don't render an empty wrapper, moves
AccountMenu.svelte into $lib/components/ui/ per the repo's own
componentization rule, adds the aria-expanded TopBar's trigger was
missing relative to MobileHeader's, and migrates the profile page
(u/[id]/+page.svelte) onto CollectionHeader — a sixth duplicate of the
same header pattern that the previous commit touched but missed.
2026-09-15 18:06:37 +02:00

141 lines
4.0 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 Search from '@lucide/svelte/icons/search';
import X from '@lucide/svelte/icons/x';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import AccountMenu from './ui/AccountMenu.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 buildHref(term: string) {
return term ? `/explore?q=${encodeURIComponent(term)}` : '/explore';
}
function runSearch(term: string) {
if (term.length > 0 && term.length < SEARCH_MIN_LENGTH) return;
goto(buildHref(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">
<div class="flex shrink-0 gap-1.5">
<button
type="button"
onclick={() => history.back()}
aria-label="Atrás"
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-surface-2 text-muted"
>
<ChevronRight class="h-[15px] w-[15px]" strokeWidth={1.9} />
</span>
</div>
<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-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
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 h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-line-strong transition active:scale-95"
>
{#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"
data-sveltekit-reload
class="shrink-0 rounded-full bg-accent-soft px-4 py-2 text-sm font-semibold text-ink transition hover:brightness-110"
>
Iniciar sesión
</a>
{/if}
</div>
</header>