Page tags were a first-class section of the mobile Library tab but had no home in the desktop /profile tabs, so desktop users couldn't browse their tagged pages without going through /search. Add a /profile/page-tags route that reuses PageTagsList (the same component the mobile Library renders) and a "Page tags" tab between Collections and History. The loader mirrors the library wrapper's page-tags fetch; 401 → sign-in prompt, matching the other profile sub-routes. e2e: the desktop Page-tags tab navigates to /profile/page-tags and renders the list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
4.6 KiB
Svelte
146 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/stores';
|
|
import { session } from '$lib/session.svelte';
|
|
import User from '@lucide/svelte/icons/user';
|
|
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
|
|
import KeyRound from '@lucide/svelte/icons/key-round';
|
|
import Bookmark from '@lucide/svelte/icons/bookmark';
|
|
import FolderOpen from '@lucide/svelte/icons/folder-open';
|
|
import Tag from '@lucide/svelte/icons/tag';
|
|
import History from '@lucide/svelte/icons/history';
|
|
|
|
let { children } = $props();
|
|
|
|
type Tab = {
|
|
href: string;
|
|
label: string;
|
|
icon: typeof User;
|
|
testid: string;
|
|
/** Subroutes that genuinely have nothing for guests are hidden
|
|
* until the user signs in. Preferences is shown to everyone
|
|
* because theme + reader settings work device-locally. */
|
|
guestVisible: boolean;
|
|
};
|
|
|
|
const tabs: Tab[] = [
|
|
{ href: '/profile', label: 'Overview', icon: User, testid: 'tab-overview', guestVisible: true },
|
|
{ href: '/profile/preferences', label: 'Preferences', icon: SlidersHorizontal, testid: 'tab-preferences', guestVisible: true },
|
|
{ href: '/profile/account', label: 'Account', icon: KeyRound, testid: 'tab-account', guestVisible: false },
|
|
{ href: '/profile/bookmarks', label: 'Bookmarks', icon: Bookmark, testid: 'tab-bookmarks', guestVisible: false },
|
|
{ href: '/profile/collections', label: 'Collections', icon: FolderOpen, testid: 'tab-collections', guestVisible: false },
|
|
{ href: '/profile/page-tags', label: 'Page tags', icon: Tag, testid: 'tab-page-tags', guestVisible: false },
|
|
{ href: '/profile/history', label: 'History', icon: History, testid: 'tab-history', guestVisible: false }
|
|
];
|
|
|
|
const visibleTabs = $derived(
|
|
tabs.filter((t) => session.user || t.guestVisible)
|
|
);
|
|
</script>
|
|
|
|
<header class="profile-header">
|
|
<h1>Profile</h1>
|
|
{#if !session.loaded}
|
|
<p class="welcome" data-testid="profile-loading">Loading session…</p>
|
|
{:else if session.user}
|
|
<p class="welcome">
|
|
Signed in as <strong>{session.user.username}</strong>
|
|
</p>
|
|
{:else}
|
|
<p class="welcome" data-testid="profile-guest">
|
|
Browsing as a guest — sign in to access bookmarks, collections,
|
|
and account settings.
|
|
</p>
|
|
{/if}
|
|
</header>
|
|
|
|
<nav class="tabs" aria-label="Profile sections">
|
|
{#each visibleTabs as t (t.href)}
|
|
{@const active =
|
|
t.href === '/profile'
|
|
? $page.url.pathname === '/profile'
|
|
: $page.url.pathname === t.href ||
|
|
$page.url.pathname.startsWith(t.href + '/')}
|
|
<a
|
|
class="tab"
|
|
class:active
|
|
href={t.href}
|
|
aria-current={active ? 'page' : undefined}
|
|
data-testid={t.testid}
|
|
>
|
|
<t.icon size={16} aria-hidden="true" />
|
|
<span>{t.label}</span>
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
|
|
<section class="content">
|
|
{@render children()}
|
|
</section>
|
|
|
|
<style>
|
|
.profile-header {
|
|
margin-bottom: var(--space-3);
|
|
}
|
|
|
|
.profile-header h1 {
|
|
margin: 0 0 var(--space-1);
|
|
}
|
|
|
|
.welcome {
|
|
color: var(--text-muted);
|
|
margin: 0;
|
|
font-size: var(--font-sm);
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-1);
|
|
margin-bottom: var(--space-4);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
/* Mobile uses the bottom-nav Account/Library tabs plus the account
|
|
hub for cross-section navigation. The horizontal tab strip would
|
|
compete with the bottom nav and wrap awkwardly at narrow widths,
|
|
so hide it below the breakpoint and let pages stand on their own. */
|
|
@media (max-width: 640px) {
|
|
.tabs {
|
|
display: none;
|
|
}
|
|
|
|
.profile-header {
|
|
margin-bottom: var(--space-2);
|
|
}
|
|
}
|
|
|
|
.tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
padding: var(--space-2) var(--space-3);
|
|
color: var(--text-muted);
|
|
font-size: var(--font-sm);
|
|
border-bottom: 2px solid transparent;
|
|
margin-bottom: -1px;
|
|
transition:
|
|
color var(--transition),
|
|
border-color var(--transition);
|
|
}
|
|
|
|
.tab:hover {
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.tab.active {
|
|
color: var(--primary);
|
|
border-bottom-color: var(--primary);
|
|
font-weight: var(--weight-medium);
|
|
}
|
|
|
|
.content {
|
|
margin-top: var(--space-3);
|
|
}
|
|
</style>
|