-
-
diff --git a/web/src/lib/components/Shell.test.ts b/web/src/lib/components/Shell.test.ts
index 5771f11b..ad3ac561 100644
--- a/web/src/lib/components/Shell.test.ts
+++ b/web/src/lib/components/Shell.test.ts
@@ -39,30 +39,35 @@ describe('Shell', () => {
expect(screen.getByText('alice')).toBeInTheDocument();
});
- test('main nav renders the six primary surfaces in order', () => {
+ test('main nav renders the three primary surfaces in order', () => {
render(Shell);
- const labels = ['Home', 'Artists', 'Albums', 'Liked', 'Discover', 'Playlists'];
+ // Library is a parent for Artists / Albums / Liked / History /
+ // Playlists (tabs under /library); only the parent appears here.
+ const labels = ['Home', 'Library', 'Discover'];
for (const label of labels) {
expect(screen.getByRole('link', { name: label })).toBeInTheDocument();
}
expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/');
- expect(screen.getByRole('link', { name: 'Artists' })).toHaveAttribute('href', '/library/artists');
- expect(screen.getByRole('link', { name: 'Albums' })).toHaveAttribute('href', '/library/albums');
+ expect(screen.getByRole('link', { name: 'Library' })).toHaveAttribute('href', '/library');
expect(screen.getByRole('link', { name: 'Discover' })).toHaveAttribute('href', '/discover');
- expect(screen.getByRole('link', { name: 'Playlists' })).toHaveAttribute('href', '/playlists');
});
- test('main nav omits Search, Requests, Hidden, Settings, and Admin', () => {
+ test('main nav omits Artists/Albums/Liked/History/Playlists/Search/Settings/Admin', () => {
userState.current = { id: '1', username: 'alice', is_admin: true };
render(Shell);
- // Search reaches /search via the global SearchInput, Requests is a
- // tab inside Discover, Hidden lives under Settings, Settings + Admin
- // live in the username dropdown — none belong on the main nav.
+ // Library sub-pages reach their content via the /library tab bar
+ // (in routes/library/+layout.svelte), not via the global Shell nav.
+ // Search reaches /search via the global SearchInput on the right.
+ // Settings + Admin live in the username dropdown — they're per-
+ // operator chrome, not primary surfaces.
const navLinks = screen
.getAllByRole('link')
- .filter((el) => el.closest('nav') !== null)
+ .filter((el) => el.closest('nav[aria-label="Primary"]') !== null)
.map((el) => el.textContent?.trim());
- for (const label of ['Search', 'Requests', 'Hidden', 'Settings', 'Admin']) {
+ for (const label of [
+ 'Artists', 'Albums', 'Liked', 'History', 'Playlists',
+ 'Search', 'Requests', 'Hidden', 'Settings', 'Admin'
+ ]) {
expect(navLinks).not.toContain(label);
}
});
diff --git a/web/src/lib/stores/mobileNav.svelte.ts b/web/src/lib/stores/mobileNav.svelte.ts
deleted file mode 100644
index 641750bc..00000000
--- a/web/src/lib/stores/mobileNav.svelte.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// Open/close state for the off-canvas mobile navigation drawer.
-// Mounted once from Shell.svelte; toggled by the hamburger button.
-
-let _open = $state(false);
-
-export const mobileNav = {
- get value(): boolean {
- return _open;
- }
-};
-
-export function openMobileNav(): void {
- _open = true;
-}
-
-export function closeMobileNav(): void {
- _open = false;
-}
-
-export function toggleMobileNav(): void {
- _open = !_open;
-}
diff --git a/web/src/routes/library/+layout.svelte b/web/src/routes/library/+layout.svelte
new file mode 100644
index 00000000..b231858a
--- /dev/null
+++ b/web/src/routes/library/+layout.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+ {@render children?.()}
+
diff --git a/web/src/routes/library/+page.server.ts b/web/src/routes/library/+page.server.ts
new file mode 100644
index 00000000..11a5e992
--- /dev/null
+++ b/web/src/routes/library/+page.server.ts
@@ -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');
+};
diff --git a/web/src/routes/playlists/+page.svelte b/web/src/routes/library/playlists/+page.svelte
similarity index 90%
rename from web/src/routes/playlists/+page.svelte
rename to web/src/routes/library/playlists/+page.svelte
index 32f02663..3f46edd5 100644
--- a/web/src/routes/playlists/+page.svelte
+++ b/web/src/routes/library/playlists/+page.svelte
@@ -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 @@
}
-{pageTitle('Playlists')}
+{pageTitle('Library · Playlists')}
-