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}