Redesign explore search and inline top-bar search box
- Explore: replace per-kind colored badges/tints in search results with the app's neutral accent, add an always-visible Usuarios chip, and keep the top-result type label as plain text - Move SearchBox into TopBar/MobileHeader as an inline debounced search form, dropping the standalone SearchBox component - Sync design handoff artboards with the current search UI
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
import Moon from '@lucide/svelte/icons/moon';
|
||||
import MenuItem from './ui/MenuItem.svelte';
|
||||
import GlassMenu from './ui/GlassMenu.svelte';
|
||||
import SearchBox from './SearchBox.svelte';
|
||||
import { themeMode } from '$lib/theme/mode.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -29,92 +28,86 @@
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-40 flex flex-col gap-2.5 border-b border-hairline bg-bg/85 page-x pt-3 pb-3 backdrop-blur-md sm:hidden"
|
||||
class="sticky top-0 z-40 flex h-14 items-center justify-between border-b border-hairline bg-bg/85 page-x backdrop-blur-md sm:hidden"
|
||||
>
|
||||
<div class="flex h-9 items-center justify-between">
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center gap-2 font-display text-[17.5px] font-semibold tracking-[-0.02em] text-fg"
|
||||
>
|
||||
<span
|
||||
class="h-5.5 w-5.5 rounded-[7px] bg-[image:var(--mf-logo-grad)] shadow-[var(--mf-logo-glow)]"
|
||||
></span>
|
||||
Musify
|
||||
</a>
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center gap-2 font-display text-[17.5px] font-semibold tracking-[-0.02em] text-fg"
|
||||
>
|
||||
<span
|
||||
class="h-5.5 w-5.5 rounded-[7px] bg-[image:var(--mf-logo-grad)] shadow-[var(--mf-logo-glow)]"
|
||||
></span>
|
||||
Musify
|
||||
</a>
|
||||
|
||||
<div class="relative" bind:this={menuRef}>
|
||||
{#if user}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
aria-label="Tu cuenta"
|
||||
aria-expanded={menuOpen}
|
||||
class="flex items-center rounded-full"
|
||||
>
|
||||
{#if user.picture}
|
||||
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
|
||||
{:else}
|
||||
<span
|
||||
class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase"
|
||||
>
|
||||
{user.name.charAt(0)}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if menuOpen}
|
||||
<GlassMenu class="absolute top-full right-0 mt-2 w-56 overflow-hidden">
|
||||
<MenuItem
|
||||
icon={User}
|
||||
label="Ver perfil"
|
||||
href="/u/{user.sub}"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Folder}
|
||||
label="Canciones subidas"
|
||||
href="/library"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Upload}
|
||||
label="Subir música"
|
||||
href="/upload"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
{#if accountUrl}
|
||||
<MenuItem
|
||||
icon={Settings}
|
||||
label="Ajustes"
|
||||
href={accountUrl}
|
||||
target="_blank"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
<MenuItem
|
||||
icon={themeMode.current === 'dark' ? Sun : Moon}
|
||||
label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'}
|
||||
onclick={() => themeMode.toggle()}
|
||||
/>
|
||||
<form method="POST" action="/logout" data-sveltekit-reload>
|
||||
<MenuItem icon={LogOut} label="Cerrar sesión" type="submit" />
|
||||
</form>
|
||||
</GlassMenu>
|
||||
<div class="relative" bind:this={menuRef}>
|
||||
{#if user}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
aria-label="Tu cuenta"
|
||||
aria-expanded={menuOpen}
|
||||
class="flex items-center rounded-full"
|
||||
>
|
||||
{#if user.picture}
|
||||
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
|
||||
{:else}
|
||||
<span class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase">
|
||||
{user.name.charAt(0)}
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<a
|
||||
href="/auth/login"
|
||||
data-sveltekit-reload
|
||||
aria-label="Iniciar sesión"
|
||||
class="flex items-center rounded-full p-1.5 text-fg-3 transition hover:bg-hover hover:text-fg"
|
||||
>
|
||||
<LogIn class="h-6 w-6" strokeWidth={2} />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<SearchBox class="w-full" />
|
||||
{#if menuOpen}
|
||||
<GlassMenu class="absolute top-full right-0 mt-2 w-56 overflow-hidden">
|
||||
<MenuItem
|
||||
icon={User}
|
||||
label="Ver perfil"
|
||||
href="/u/{user.sub}"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Folder}
|
||||
label="Canciones subidas"
|
||||
href="/library"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={Upload}
|
||||
label="Subir música"
|
||||
href="/upload"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
{#if accountUrl}
|
||||
<MenuItem
|
||||
icon={Settings}
|
||||
label="Ajustes"
|
||||
href={accountUrl}
|
||||
target="_blank"
|
||||
onclick={() => (menuOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
<MenuItem
|
||||
icon={themeMode.current === 'dark' ? Sun : Moon}
|
||||
label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'}
|
||||
onclick={() => themeMode.toggle()}
|
||||
/>
|
||||
<form method="POST" action="/logout" data-sveltekit-reload>
|
||||
<MenuItem icon={LogOut} label="Cerrar sesión" type="submit" />
|
||||
</form>
|
||||
</GlassMenu>
|
||||
{/if}
|
||||
{:else}
|
||||
<a
|
||||
href="/auth/login"
|
||||
data-sveltekit-reload
|
||||
aria-label="Iniciar sesión"
|
||||
class="flex items-center rounded-full p-1.5 text-fg-3 transition hover:bg-hover hover:text-fg"
|
||||
>
|
||||
<LogIn class="h-6 w-6" strokeWidth={2} />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<svelte:window onclick={onDocumentClick} />
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { SEARCH_DEBOUNCE_MS, SEARCH_MIN_LENGTH } from '$lib/config';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
let { class: klass = '', placeholder = 'Canciones, artistas, álbumes o playlists' }: Props =
|
||||
$props();
|
||||
|
||||
let query = $state('');
|
||||
let searchInput: HTMLInputElement | undefined = $state();
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
function navigateSearch() {
|
||||
const term = query.trim();
|
||||
goto(term ? `/explore?q=${encodeURIComponent(term)}` : '/explore', {
|
||||
replaceState: page.url.pathname === '/explore',
|
||||
keepFocus: true,
|
||||
noScroll: true
|
||||
});
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
clearTimeout(searchTimeout);
|
||||
const term = query.trim();
|
||||
const onExplore = page.url.pathname === '/explore';
|
||||
if (term.length > 0 && term.length < SEARCH_MIN_LENGTH) return;
|
||||
if (term.length === 0 && !onExplore) return;
|
||||
searchTimeout = setTimeout(navigateSearch, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function clearQuery() {
|
||||
clearTimeout(searchTimeout);
|
||||
query = '';
|
||||
if (page.url.pathname === '/explore') navigateSearch();
|
||||
searchInput?.focus();
|
||||
}
|
||||
|
||||
function onSubmit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
clearTimeout(searchTimeout);
|
||||
navigateSearch();
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Enter') return;
|
||||
event.preventDefault();
|
||||
clearTimeout(searchTimeout);
|
||||
navigateSearch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative min-w-0 {klass}">
|
||||
<form onsubmit={onSubmit}>
|
||||
<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%]"
|
||||
>
|
||||
<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}
|
||||
onkeydown={onKeydown}
|
||||
{placeholder}
|
||||
class="w-full min-w-0 bg-transparent text-[13px] 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>
|
||||
@@ -1,5 +1,10 @@
|
||||
<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 LogOut from '@lucide/svelte/icons/log-out';
|
||||
@@ -9,7 +14,6 @@
|
||||
import Moon from '@lucide/svelte/icons/moon';
|
||||
import MenuItem from './ui/MenuItem.svelte';
|
||||
import GlassMenu from './ui/GlassMenu.svelte';
|
||||
import SearchBox from './SearchBox.svelte';
|
||||
import { themeMode } from '$lib/theme/mode.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -19,8 +23,44 @@
|
||||
|
||||
let { user, accountUrl }: Props = $props();
|
||||
|
||||
let query = $derived(
|
||||
page.url.pathname === '/explore' ? (page.url.searchParams.get('q') ?? '') : ''
|
||||
);
|
||||
let menuOpen = $state(false);
|
||||
let menuRef: HTMLDivElement | undefined = $state();
|
||||
let searchInput: HTMLInputElement | undefined = $state();
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
function onDocumentClick(event: MouseEvent) {
|
||||
if (menuRef && !menuRef.contains(event.target as Node)) menuOpen = false;
|
||||
@@ -45,7 +85,31 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<SearchBox class="max-w-[480px] flex-1" />
|
||||
<form onsubmit={onSubmit} class="max-w-[480px] min-w-0 flex-1">
|
||||
<label
|
||||
class="flex h-9.5 w-full items-center gap-2.5 rounded-[11px] bg-white/[3.5%] px-3.5 transition focus-within:bg-white/[6%]"
|
||||
>
|
||||
<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-[13px] 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>
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" module>
|
||||
export type SearchResultKind = 'track' | 'album' | 'playlist' | 'user';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import ExplicitBadge from './ExplicitBadge.svelte';
|
||||
|
||||
const RADIUS: Record<SearchResultKind, string> = {
|
||||
track: 'rounded-control',
|
||||
album: 'rounded-[6px]',
|
||||
playlist: 'rounded-[13px]',
|
||||
user: 'rounded-full'
|
||||
};
|
||||
|
||||
interface Props {
|
||||
kind: SearchResultKind;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
meta?: string;
|
||||
explicit?: boolean;
|
||||
href?: string;
|
||||
onclick?: () => void;
|
||||
oncontextmenu?: (event: MouseEvent) => void;
|
||||
art: Snippet<[string]>;
|
||||
}
|
||||
|
||||
let {
|
||||
kind,
|
||||
title,
|
||||
subtitle,
|
||||
meta,
|
||||
explicit = false,
|
||||
href,
|
||||
onclick,
|
||||
oncontextmenu,
|
||||
art
|
||||
}: Props = $props();
|
||||
|
||||
const artClass = $derived(
|
||||
`relative h-13 w-13 shrink-0 overflow-hidden shadow-art ${RADIUS[kind]}`
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet content()}
|
||||
{@render art(artClass)}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 items-center gap-1.75">
|
||||
{#if explicit}
|
||||
<ExplicitBadge />
|
||||
{/if}
|
||||
<span class="truncate text-[13px] font-medium text-fg">{title}</span>
|
||||
</div>
|
||||
{#if subtitle}
|
||||
<div class="mt-0.75 truncate text-[11.5px] text-fg-3">{subtitle}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if meta}
|
||||
<span class="shrink-0 text-[11.5px] text-muted tabular-nums">{meta}</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
{oncontextmenu}
|
||||
class="group/row flex w-full min-w-0 items-center gap-3.5 rounded-control p-2 pr-4 text-left transition hover:bg-surface"
|
||||
>
|
||||
{@render content()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
{onclick}
|
||||
{oncontextmenu}
|
||||
class="group/row flex w-full min-w-0 items-center gap-3.5 rounded-control p-2 pr-4 text-left transition hover:bg-surface focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none"
|
||||
>
|
||||
{@render content()}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
{#snippet heading()}
|
||||
<h2 class="font-display text-[17.5px] font-semibold tracking-[-0.025em] text-fg">
|
||||
<h2 class="font-display text-[17.5px] font-medium tracking-[-0.025em] text-fg">
|
||||
{title}
|
||||
</h2>
|
||||
{#if subtitle}
|
||||
|
||||
Reference in New Issue
Block a user