From 22ac86f200d96bba3b90d97d8e95b8109b1238cb Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 22:23:56 -0400 Subject: [PATCH] feat(web): trim main nav; pair Discover+Requests; move chrome to user menu The main nav was carrying surfaces that didn't belong on it: - Search led to an empty page when clicked (you only ever want to land there from the global SearchInput typing into /search?q=...). Drop it. - Requests is downstream of Discover (you discover, then you request). Lift both onto a shared horizontal tab strip; keep their URLs so bookmarks survive. New DiscoverTabs component used by both pages. - Settings + Admin are operator chrome, not primary surfaces. Move them into the username dropdown alongside Log out, with Admin gated on is_admin. Users see Settings + Log out; admins see Settings + Admin + Log out. Final main nav: Home / Artists / Albums / Liked / Discover / Playlists. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/lib/components/DiscoverTabs.svelte | 36 +++++++++++++ web/src/lib/components/DiscoverTabs.test.ts | 35 ++++++++++++ web/src/lib/components/Shell.svelte | 40 +++++++++----- web/src/lib/components/Shell.test.ts | 60 +++++++++++---------- web/src/routes/discover/+page.svelte | 5 +- web/src/routes/requests/+page.svelte | 5 +- 6 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 web/src/lib/components/DiscoverTabs.svelte create mode 100644 web/src/lib/components/DiscoverTabs.test.ts diff --git a/web/src/lib/components/DiscoverTabs.svelte b/web/src/lib/components/DiscoverTabs.svelte new file mode 100644 index 00000000..300a97ea --- /dev/null +++ b/web/src/lib/components/DiscoverTabs.svelte @@ -0,0 +1,36 @@ + + + diff --git a/web/src/lib/components/DiscoverTabs.test.ts b/web/src/lib/components/DiscoverTabs.test.ts new file mode 100644 index 00000000..ebcc9e54 --- /dev/null +++ b/web/src/lib/components/DiscoverTabs.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; + +const state = vi.hoisted(() => ({ + pageUrl: new URL('http://localhost/discover') +})); + +vi.mock('$app/state', () => ({ + page: { get url() { return state.pageUrl; } } +})); + +import DiscoverTabs from './DiscoverTabs.svelte'; + +describe('DiscoverTabs', () => { + test('Discover tab is current on /discover', () => { + state.pageUrl = new URL('http://localhost/discover'); + render(DiscoverTabs); + expect(screen.getByRole('link', { name: /discover/i })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByRole('link', { name: /requests/i })).not.toHaveAttribute('aria-current'); + }); + + test('Requests tab is current on /requests', () => { + state.pageUrl = new URL('http://localhost/requests'); + render(DiscoverTabs); + expect(screen.getByRole('link', { name: /requests/i })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByRole('link', { name: /discover/i })).not.toHaveAttribute('aria-current'); + }); + + test('renders both tabs in order', () => { + state.pageUrl = new URL('http://localhost/discover'); + render(DiscoverTabs); + const labels = screen.getAllByRole('link').map((l) => l.textContent?.trim()); + expect(labels).toEqual(['Discover', 'Requests']); + }); +}); diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte index 9d121751..ddfeb032 100644 --- a/web/src/lib/components/Shell.svelte +++ b/web/src/lib/components/Shell.svelte @@ -21,24 +21,17 @@ 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. const navItems = [ { href: '/', label: 'Home' }, { href: '/library/artists', label: 'Artists' }, { href: '/library/albums', label: 'Albums' }, { href: '/library/liked', label: 'Liked' }, - { href: '/search', label: 'Search' }, { href: '/discover', label: 'Discover' }, - { href: '/requests', label: 'Requests' }, - { href: '/playlists', label: 'Playlists' }, - { href: '/settings', label: 'Settings' } + { href: '/playlists', label: 'Playlists' } ]; - - // Admin link sits between Playlists and Settings, only visible to admins. - const visibleNavItems = $derived( - user.value?.is_admin - ? [...navItems.slice(0, -1), { href: '/admin', label: 'Admin' }, navItems[navItems.length - 1]] - : navItems - ); (menuOpen = false)} onkeydown={(e) => e.key === 'Escape' && (menuOpen = false)} /> @@ -59,15 +52,34 @@ {#if menuOpen}
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="menu" tabindex="-1" > + (menuOpen = false)} + > + Settings + + {#if user.value?.is_admin} + (menuOpen = false)} + > + Admin + + {/if}