feat(web): top-bar centered nav + Library tab page (replace sidebar)
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:
2026-06-01 10:07:02 -04:00
parent 9dc707f1c8
commit 6e6fbc7856
10 changed files with 124 additions and 296 deletions
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts">
import { page } from '$app/state';
let { children } = $props<{ children: import('svelte').Snippet }>();
// Tab bar for the Library section. Mirrors Android's LibraryScreen
// (artists / albums / liked / history / playlists) — Playlists lives
// here so the operator can find their personal collection in one
// place rather than tracking a separate top-level route.
const tabs = [
{ href: '/library/artists', label: 'Artists' },
{ href: '/library/albums', label: 'Albums' },
{ href: '/library/liked', label: 'Liked' },
{ href: '/library/history', label: 'History' },
{ href: '/library/playlists', label: 'Playlists' }
];
function isActiveTab(href: string): boolean {
return page.url.pathname === href || page.url.pathname.startsWith(href + '/');
}
</script>
<div class="space-y-4">
<nav aria-label="Library sections" class="border-b border-border">
<ul class="flex gap-1 overflow-x-auto">
{#each tabs as tab}
{@const active = isActiveTab(tab.href)}
<li>
<a
href={tab.href}
class="block px-3 md:px-4 py-3 text-sm whitespace-nowrap border-b-2
{active
? 'border-accent text-text-primary'
: 'border-transparent text-text-secondary hover:text-text-primary'}"
aria-current={active ? 'page' : undefined}
>
{tab.label}
</a>
</li>
{/each}
</ul>
</nav>
{@render children?.()}
</div>
+9
View File
@@ -0,0 +1,9 @@
import { redirect } from '@sveltejs/kit';
// /library has no content of its own — the tab bar (in +layout.svelte)
// always renders the active sub-page. Bare hits go to the default tab
// (Artists, matching Android's LibraryScreen default). 308 = permanent
// + preserve method, so SPA navigations and direct loads behave the same.
export const load = () => {
throw redirect(308, '/library/artists');
};
@@ -32,6 +32,8 @@
try {
const p = await createPlaylist({ name: newName.trim() });
await queryClient.invalidateQueries({ queryKey: qk.playlists() });
// Detail route stays at /playlists/[id] (matches the API URL);
// only the LIST view moved under /library.
goto(`/playlists/${p.id}`);
} catch (e: unknown) {
createError = (e as { message?: string })?.message ?? 'Could not create.';
@@ -43,11 +45,11 @@
}
</script>
<svelte:head><title>{pageTitle('Playlists')}</title></svelte:head>
<svelte:head><title>{pageTitle('Library · Playlists')}</title></svelte:head>
<div class="mx-auto max-w-6xl px-4 py-6">
<header class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-medium text-text-primary">Playlists</h1>
<div class="space-y-4">
<header class="flex items-center justify-between">
<h1 class="font-display text-2xl font-medium text-text-primary">Playlists</h1>
<button
type="button"
onclick={() => (creating = true)}
@@ -59,7 +61,7 @@
</header>
{#if creating}
<div class="mb-6 rounded-md border border-border bg-surface p-4">
<div class="rounded-md border border-border bg-surface p-4">
<label class="block text-sm font-medium text-text-primary">
Name
<input
@@ -99,7 +101,7 @@
{:else if playlistsQuery?.isError}
<ApiErrorBanner error={playlistsQuery.error} onRetry={() => playlistsQuery.refetch()} />
{:else if playlistsQuery?.data}
<section class="mb-8">
<section>
<h2 class="mb-3 text-sm font-medium uppercase tracking-wide text-text-muted">Your playlists</h2>
{#if playlistsQuery.data.owned.length === 0}
<p class="text-sm text-text-muted">No playlists yet. Click "New playlist" to start one.</p>
@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { mockQuery } from '../../test-utils/query';
import { mockQuery } from '../../../test-utils/query';
import type { Playlist } from '$lib/api/types';
vi.mock('$lib/api/playlists', () => ({
+10
View File
@@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit';
// /playlists (list) moved to /library/playlists on 2026-06-01 as part of
// the nav restructure (Scribe 518). The detail route /playlists/[id]
// still lives here at /routes/playlists/[id]/+page.svelte — that URL
// matches the server API shape and is unchanged.
// 308 = permanent + preserve method; old bookmarks land on the new URL.
export const load = () => {
throw redirect(308, '/library/playlists');
};