Merge pull request #679 from kettui/feat/nav-links
Make side-nav buttons actual link elements
This commit is contained in:
commit
fa3b7fbef3
5 changed files with 66 additions and 54 deletions
|
|
@ -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…
Reference in a new issue