From cadd78603c423b49df19ff23176ecefb84e1b1d1 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sat, 23 May 2026 12:47:23 +0300 Subject: [PATCH] fix(webui): make sidebar nav SPA links - convert the sidebar nav to real links with URL-driven state - intercept left-clicks so internal navigation stays in-app while preserving native browser link behavior - keep artist-detail transitions param-aware and update route tests --- webui/index.html | 68 ++++++++++++++++---------------- webui/static/init.js | 12 ++---- webui/static/shell-bridge.js | 24 +++++++---- webui/static/style.css | 2 + webui/tests/issues.smoke.spec.ts | 14 +++++-- 5 files changed, 66 insertions(+), 54 deletions(-) diff --git a/webui/index.html b/webui/index.html index fac604ed..6df47a2a 100644 --- a/webui/index.html +++ b/webui/index.html @@ -194,78 +194,78 @@ diff --git a/webui/static/init.js b/webui/static/init.js index 0465f70d..2f7c1239 100644 --- a/webui/static/init.js +++ b/webui/static/init.js @@ -2136,15 +2136,9 @@ function initApp() { // =============================== function initializeNavigation() { - const navButtons = document.querySelectorAll('.nav-button'); - - navButtons.forEach(button => { - button.addEventListener('click', () => { - const page = button.getAttribute('data-page'); - navigateToPage(page); - }); - }); - + // Sidebar navigation is now driven by native link navigation. + // Page activation and active-state styling are synchronized from the + // current URL by the shell bridge and route controllers. } const _DEEPLINK_VALID_PAGES = new Set([ diff --git a/webui/static/shell-bridge.js b/webui/static/shell-bridge.js index 66127869..7db84a76 100644 --- a/webui/static/shell-bridge.js +++ b/webui/static/shell-bridge.js @@ -22,15 +22,18 @@ function showLegacyPage(pageId) { function setActivePageChrome(pageId) { document.querySelectorAll('.nav-button').forEach(btn => { btn.classList.remove('active'); + btn.removeAttribute('aria-current'); }); const navButton = document.querySelector(`[data-page="${pageId}"]`); if (navButton) { navButton.classList.add('active'); + navButton.setAttribute('aria-current', 'page'); } else if (pageId === 'artist-detail') { // Artist detail is a Library context, so keep the sidebar anchored there. const libraryBtn = document.querySelector('[data-page="library"]'); if (libraryBtn) { libraryBtn.classList.add('active'); + libraryBtn.setAttribute('aria-current', 'page'); } } currentPage = pageId; @@ -181,35 +184,40 @@ function _handleShellLinkClick(event) { const anchor = event.target?.closest?.('a[href]'); if (!anchor || (anchor.target && anchor.target !== '_self')) return; + if (anchor.hasAttribute('download')) return; const href = anchor.getAttribute('href'); if (!href || href === '#' || href.startsWith('javascript:')) return; - const router = getWebRouter(); - if (!router?.navigateToPage) return; - const pathname = anchor.pathname || new URL(anchor.href, window.location.href).pathname; + const navPageId = anchor.matches('.nav-button[data-page]') ? anchor.getAttribute('data-page') : null; + if (navPageId) { + event.preventDefault(); + void navigateToPage(navPageId); + return; + } if (pathname.startsWith('/artist-detail/')) { - _handleArtistDetailLinkClick(event, pathname, router); + _handleArtistDetailLinkClick(event, pathname); return; } } -function _handleArtistDetailLinkClick(event, pathname, router) { +function _handleArtistDetailLinkClick(event, pathname) { const parts = pathname.split('/').filter(Boolean); if (parts.length < 3) return; - // Keep the semantic link, but hand the click back to TanStack so artist - // detail navigations stay in the SPA when the router is available. + // Keep the semantic link, but hand the click back to the SPA router so + // artist detail navigations stay in-app when the link is left-clicked. const source = decodeURIComponent(parts[1] || ''); const artistId = decodeURIComponent(parts.slice(2).join('/')); if (!source || !artistId) return; event.preventDefault(); - void router.navigateToPage('artist-detail', { + void navigateToPage('artist-detail', { artistId, artistSource: source, + forceReload: true, }); } diff --git a/webui/static/style.css b/webui/static/style.css index 24bf7425..bbdc9371 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -380,11 +380,13 @@ body { width: 216px; background: transparent; border: 1px solid transparent; + color: inherit; border-radius: 12px; cursor: pointer; display: flex; align-items: center; gap: 14px; + text-decoration: none; padding: 0 16px; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); font-family: 'SF Pro Text', -apple-system, sans-serif; diff --git a/webui/tests/issues.smoke.spec.ts b/webui/tests/issues.smoke.spec.ts index c816b8e6..ebac18f5 100644 --- a/webui/tests/issues.smoke.spec.ts +++ b/webui/tests/issues.smoke.spec.ts @@ -30,7 +30,7 @@ async function waitForShellRoute(page: Page, pageId: string) { function getExpectedNavPage(pageId: ShellPageId): string { if (pageId === 'artist-detail') { - return ''; + return 'library'; } return pageId; @@ -95,7 +95,15 @@ test('browser history restores top-level routes', async ({ page, baseURL }) => { await page.goto(new URL('/discover', baseURL).toString(), { waitUntil: 'domcontentloaded' }); await waitForShellRoute(page, 'discover'); - await page.getByRole('button', { name: 'Issues' }).click(); + await page.evaluate(() => { + (window as typeof window & { __spaNavMarker?: string }).__spaNavMarker = 'persist'; + }); + await page.getByRole('link', { name: 'Issues' }).click(); + await expect + .poll(async () => + page.evaluate(() => (window as typeof window & { __spaNavMarker?: string }).__spaNavMarker ?? null), + ) + .toBe('persist'); await waitForShellRoute(page, 'issues'); await expect(page).toHaveURL(/\/issues(?:\?status=open&category=all)?$/); @@ -125,7 +133,7 @@ test('browser history leaves artist detail when going back to library', async ({ await page.locator('.library-artist-card').first().click(); await waitForShellRoute(page, 'artist-detail'); - await expect(page).toHaveURL(/\/artist-detail$/); + await expect(page).toHaveURL(/\/artist-detail\/library\/[^/]+$/); await page.goBack(); await waitForShellRoute(page, 'library');