Merge pull request #679 from kettui/feat/nav-links

Make side-nav buttons actual link elements
pull/685/head
BoulderBadgeDad 1 week ago committed by GitHub
commit fa3b7fbef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -194,78 +194,78 @@
<!-- Navigation Section -->
<nav class="sidebar-nav">
<button class="nav-button" data-page="dashboard">
<a class="nav-button" data-page="dashboard" href="/dashboard">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="4" rx="1"/><rect x="3" y="14" width="7" height="4" rx="1"/><rect x="14" y="11" width="7" height="7" rx="1"/><line x1="5" y1="20" x2="5" y2="22"/><line x1="8" y1="19" x2="8" y2="22"/></svg></span>
<span class="nav-text">Dashboard</span>
</button>
<button class="nav-button" data-page="sync">
</a>
<a class="nav-button" data-page="sync" href="/sync">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg></span>
<span class="nav-text">Sync</span>
</button>
<button class="nav-button" data-page="search">
</a>
<a class="nav-button" data-page="search" href="/search">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><polyline points="8 11 11 14 14 11"/></svg></span>
<span class="nav-text">Search</span>
</button>
<button class="nav-button" data-page="discover">
</a>
<a class="nav-button" data-page="discover" href="/discover">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="currentColor" opacity="0.2" stroke="currentColor"/></svg></span>
<span class="nav-text">Discover</span>
</button>
<button class="nav-button" data-page="playlist-explorer">
</a>
<a class="nav-button" data-page="playlist-explorer" href="/playlist-explorer">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="3"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="12" x2="5" y2="18"/><line x1="12" y1="12" x2="19" y2="18"/><circle cx="5" cy="19" r="2"/><circle cx="19" cy="19" r="2"/><line x1="12" y1="12" x2="12" y2="18"/><circle cx="12" cy="19" r="2"/></svg></span>
<span class="nav-text">Explorer</span>
</button>
<button class="nav-button" data-page="watchlist">
</a>
<a class="nav-button" data-page="watchlist" href="/watchlist">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span>
<span class="nav-text">Watchlist</span>
<span class="dl-nav-badge hidden" id="watchlist-nav-badge">0</span>
</button>
<button class="nav-button" data-page="wishlist">
</a>
<a class="nav-button" data-page="wishlist" href="/wishlist">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></span>
<span class="nav-text">Wishlist</span>
<span class="dl-nav-badge hidden" id="wishlist-nav-badge">0</span>
</button>
<button class="nav-button" data-page="automations">
</a>
<a class="nav-button" data-page="automations" href="/automations">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>
<span class="nav-text">Automations</span>
</button>
<button class="nav-button" data-page="active-downloads">
</a>
<a class="nav-button" data-page="active-downloads" href="/active-downloads">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></span>
<span class="nav-text">Downloads</span>
<span class="dl-nav-badge hidden" id="dl-nav-badge">0</span>
</button>
<button class="nav-button" data-page="import">
</a>
<a class="nav-button" data-page="import" href="/import">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/><polyline points="7 9 12 4 17 9"/><line x1="12" y1="4" x2="12" y2="16"/></svg></span>
<span class="nav-text">Import</span>
</button>
<button class="nav-button" data-page="library">
</a>
<a class="nav-button" data-page="library" href="/library">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="14" y2="11"/></svg></span>
<span class="nav-text">Library</span>
</button>
<button class="nav-button" data-page="tools">
</a>
<a class="nav-button" data-page="tools" href="/tools">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span>
<span class="nav-text">Tools</span>
</button>
<button class="nav-button" data-page="stats">
</a>
<a class="nav-button" data-page="stats" href="/stats">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg></span>
<span class="nav-text">Stats</span>
</button>
<button class="nav-button" data-page="settings">
</a>
<a class="nav-button" data-page="settings" href="/settings">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
<span class="nav-text">Settings</span>
</button>
<button class="nav-button" data-page="issues">
</a>
<a class="nav-button" data-page="issues" href="/issues">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></span>
<span class="nav-text">Issues</span>
<span class="issues-nav-badge hidden" id="issues-nav-badge">0</span>
</button>
<button class="nav-button" data-page="help">
</a>
<a class="nav-button" data-page="help" href="/help">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<span class="nav-text">Help & Docs</span>
</button>
<button class="nav-button" data-page="hydrabase" id="hydrabase-nav" style="display: none;">
</a>
<a class="nav-button" data-page="hydrabase" id="hydrabase-nav" href="/hydrabase" style="display: none;">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></span>
<span class="nav-text">Hydrabase</span>
</button>
</a>
</nav>
<!-- Spacer -->

@ -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([

@ -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,
});
}

@ -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;

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

Loading…
Cancel
Save