feat(web): top-bar centered nav + Library tab page (replace sidebar)
test-web / test (push) Successful in 32s
test-web / test (push) Successful in 32s
Operator 2026-06-01: "navigation layout and library sections are
what I'd like to have implemented as it seems better than our
current navbar solution. I think I'd like to have these nav options
moved into the top bar centered."
Top-bar restructure:
- Centered nav (replaces the 192dp left sidebar): Home / Library /
Discover, with icons + labels. Labels collapse below sm breakpoint
so the bar stays icon-only on small viewports.
- Right side (search input + user dropdown) unchanged.
- Hamburger button + MobileNavDrawer + the mobileNav store all
removed - the centered nav lives at all viewport sizes.
Library page restructure (mirrors Android LibraryScreen):
- New routes/library/+layout.svelte renders a tab bar across the
five Library sub-pages: Artists / Albums / Liked / History /
Playlists. Active tab gets an accent underline + onSurface text.
- routes/library/+page.server.ts redirects bare /library to
/library/artists (Android default tab).
- /playlists (list) moved to /library/playlists; old URL gets a 308
redirect (routes/playlists/+page.server.ts) so existing bookmarks
land on the new location. /playlists/[id] (detail) is unchanged -
matches the server API URL shape.
Deleted: Shell's sidebar markup, MobileNavDrawer.{svelte,test.ts},
the mobileNav store, the old routes/playlists/+page.svelte. Shell
test rewritten to assert the new 3-item centered nav; playlists
test moved next to its new +page.svelte and its test-utils import
path updated.
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { ChevronDown, Menu } from 'lucide-svelte';
|
||||
import { ChevronDown, Compass, House, LibraryBig } from 'lucide-svelte';
|
||||
import { user, logout } from '$lib/auth/store.svelte';
|
||||
import { player } from '$lib/player/store.svelte';
|
||||
import { appName } from '$lib/branding';
|
||||
import PlayerBar from './PlayerBar.svelte';
|
||||
import SearchInput from './SearchInput.svelte';
|
||||
import MobileNavDrawer from './MobileNavDrawer.svelte';
|
||||
import { toggleMobileNav } from '$lib/stores/mobileNav.svelte';
|
||||
|
||||
let { children } = $props<{ children: import('svelte').Snippet }>();
|
||||
|
||||
@@ -43,39 +41,46 @@
|
||||
return page.url.pathname.startsWith(href);
|
||||
}
|
||||
|
||||
// Search reaches /search via the global SearchInput in the header (not the
|
||||
// nav). Requests is a tab inside Discover. Settings + Admin live in the
|
||||
// username dropdown — they're per-operator chrome, not primary surfaces.
|
||||
// Primary nav. Sits centered in the top bar (replaces the prior left
|
||||
// sidebar). Mirrors the Android top-app-bar treatment: three first-class
|
||||
// destinations, plus Library acting as a parent for Artists / Albums /
|
||||
// Liked / History / Playlists (tabs at /library/+layout.svelte).
|
||||
// Search + user menu live on the right side of the header (unchanged).
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/library/artists', label: 'Artists' },
|
||||
{ href: '/library/albums', label: 'Albums' },
|
||||
{ href: '/library/liked', label: 'Liked' },
|
||||
{ href: '/library/history', label: 'History' },
|
||||
{ href: '/discover', label: 'Discover' },
|
||||
{ href: '/playlists', label: 'Playlists' }
|
||||
{ href: '/', label: 'Home', icon: House },
|
||||
{ href: '/library', label: 'Library', icon: LibraryBig },
|
||||
{ href: '/discover', label: 'Discover', icon: Compass }
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={handleWindowClick} onkeydown={(e) => e.key === 'Escape' && (menuOpen = false)} />
|
||||
|
||||
<div class="grid h-screen grid-cols-[auto_1fr] grid-rows-[auto_1fr_auto] bg-background text-text-primary">
|
||||
<header class="col-span-2 flex items-center gap-2 md:gap-4 border-b border-border bg-surface px-3 md:px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Open navigation"
|
||||
onclick={toggleMobileNav}
|
||||
class="md:hidden flex items-center justify-center min-h-[44px] min-w-[44px] -ml-1
|
||||
rounded text-text-secondary hover:text-text-primary
|
||||
focus-visible:ring-2 focus-visible:ring-accent"
|
||||
>
|
||||
<Menu size={22} strokeWidth={1.5} />
|
||||
</button>
|
||||
<div class="font-semibold text-sm md:text-base">{appName()}</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="grid h-screen grid-rows-[auto_1fr_auto] bg-background text-text-primary">
|
||||
<header class="flex items-center gap-3 md:gap-6 border-b border-border bg-surface px-3 md:px-4 py-2">
|
||||
<a href="/" class="font-semibold text-sm md:text-base whitespace-nowrap">{appName()}</a>
|
||||
|
||||
<nav aria-label="Primary" class="flex flex-1 items-center justify-center gap-1 md:gap-2">
|
||||
{#each navItems as item}
|
||||
{@const active = isActive(item.href)}
|
||||
<a
|
||||
href={item.href}
|
||||
class="group flex items-center gap-2 rounded px-2 md:px-3 py-2 min-h-[44px]
|
||||
text-text-secondary hover:text-text-primary hover:bg-surface-hover
|
||||
focus-visible:ring-2 focus-visible:ring-accent
|
||||
{active ? 'text-text-primary bg-surface-hover' : ''}"
|
||||
aria-current={active ? 'page' : undefined}
|
||||
>
|
||||
<item.icon size={18} strokeWidth={1.75} aria-hidden="true" />
|
||||
<span class="hidden sm:inline">{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="hidden md:block min-w-0 max-w-xs flex-shrink">
|
||||
<SearchInput />
|
||||
</div>
|
||||
<div class="relative">
|
||||
|
||||
<div class="relative flex-shrink-0">
|
||||
<button
|
||||
bind:this={menuButtonRef}
|
||||
type="button"
|
||||
@@ -130,31 +135,11 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="hidden w-48 border-r border-border bg-surface md:block">
|
||||
<ul class="p-2">
|
||||
{#each navItems as item}
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class="block rounded px-3 py-2 hover:bg-surface-hover"
|
||||
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main class="overflow-y-auto p-4 md:col-start-2">
|
||||
<main class="overflow-y-auto p-4">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
|
||||
{#if player.current}
|
||||
<div class="col-span-2">
|
||||
<PlayerBar />
|
||||
</div>
|
||||
<PlayerBar />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<MobileNavDrawer />
|
||||
|
||||
Reference in New Issue
Block a user