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');