feat(frontend): mobile account hub + library wrapper (0.60.0)

Phase 5 (final) of the mobile redesign: /profile/account becomes an
inset-grouped iOS-style hub on mobile and the new /library route hosts
a SegmentedControl over Bookmarks / Collections / History. Profile-
layout horizontal tabs hide below 640px. Desktop layout is unchanged.

- New /library/+page.ts loads bookmarks, collections, and read-history
  in parallel so segmented-control tabs swap instantly without a new
  round trip. 401 → unauthenticated path with sign-in prompt.
- New /library/+page.svelte renders the SegmentedControl with ?tab=
  as the source of truth. Bookmarks reuses the existing BookmarkList,
  Collections reuses CollectionsGrid, History inlines a slim cover +
  title + "Continue Ch. N" row list. `goto(..., { replaceState: true })`
  drives the URL — plain `replaceState` from $app/navigation doesn't
  reliably re-trigger $page-derived state.
- BottomNav's Library tab now points at /library (Phase 1 placeholder
  was /bookmarks). The Phase 1 mobile-chrome spec is updated to expect
  the new target and mocks the additional library data endpoints.
- /profile/+layout.svelte hides the horizontal tabs below 640px — the
  bottom-nav Library/Account tabs plus the account hub carry mobile
  cross-section navigation.
- /profile/account/+page.svelte gains a mobile hub: a centered avatar
  + username + "Member since" header, a row group with Profile /
  Preferences / Change password rows (the last opening a bottom
  Sheet hosting the existing password form snippet), and a separate
  group with a red "Log out" row that reuses the layout's logout
  flow (logout API → session.setUser(null) → preferences.clearForLogout
  → goto('/login')). Desktop keeps the inline card with the form.
- matchMedia gates the hub vs. desktop card so the password form
  testids never duplicate on the page — the snippet is rendered in
  exactly one mount at a time.
- 9 Playwright tests cover the Library nav handoff, segmented-control
  sub-tab swap with URL, sign-in CTAs on both routes, account hub
  composition, password sheet open flow, logout flow (POST /auth/
  logout + redirect to /login), profile tabs hiding on mobile, and
  the desktop regression where the inline card is the only password
  surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 11:35:07 +02:00
parent f5842510b7
commit 1e3fd27308
10 changed files with 790 additions and 25 deletions

View File

@@ -39,6 +39,7 @@
'/upload': 'Mangalord | Upload',
'/bookmarks': 'Mangalord | Bookmarks',
'/collections': 'Mangalord | Collections',
'/library': 'Mangalord | Library',
'/profile': 'Mangalord | Profile',
'/profile/account': 'Mangalord | Account',
'/profile/bookmarks': 'Mangalord | Bookmarks',
@@ -53,19 +54,21 @@
const layoutTitle = $derived(STATIC_TITLES[$page.route?.id ?? ''] ?? 'Mangalord');
// Mobile bottom-nav tabs. Library consolidates Bookmarks / Collections /
// History — it currently links to /bookmarks as the closest existing
// route; Phase 5 introduces /library with internal segmented sub-tabs
// and this href flips at that point. Browse temporarily shares Home's
// destination until the curated-feed split lands.
// Mobile bottom-nav tabs. Library is now its own wrapper at /library
// (Phase 5) hosting a SegmentedControl over Bookmarks / Collections /
// History; the older top-level /bookmarks + /collections routes stay
// active for desktop users and as deep-link targets, so they remain
// in the `match` set. Browse temporarily shares Home's destination
// until the curated-feed split lands.
const MOBILE_TABS: BottomNavTab[] = [
{ label: 'Home', href: '/', icon: House, match: [] },
{ label: 'Browse', href: '/', icon: Search, match: [] },
{
label: 'Library',
href: '/bookmarks',
href: '/library',
icon: BookOpen,
match: [
'/library',
'/bookmarks',
'/collections',
'/profile/bookmarks',