Skip to content

Epic 35 — Navigation Chrome UX Refresh (Bell, Theme, Responsive Nav)

Decision (user, 2026-06-08): Tidy up the app's navigation chrome so it reads like a modern product. Three changes, all frontend-only (no backend/schema work — the notifications API from Epic 33 already exists):

  1. Alerts → top-right bell. The notifications/alerts entry leaves the sidebar/bottom-bar nav list and becomes a single bell with an unread counter in the top-right header (a notification dropdown panel), the way every other product surfaces alerts.
  2. Theme toggle leaves the navbar. The ThemeToggle is removed from the header (and admin header); it lives only in Profile → Settings going forward.
  3. Responsive nav split. Desktop keeps the full sidebar with every link. Mobile shows a compact bottom bar — Home, Jobs, Messages — plus a hamburger that slides an overlay drawer in from the left containing the full link list (the same set the desktop sidebar shows).

Why: Today every nav item (home, jobs, applications, talents/pets, messages, alerts, profile, help, report-bug, admin) is pushed into a single list that renders identically as the desktop sidebar and the mobile bottom bar (packages/design-system/src/components/layout/AppShell.tsx:129-143), so the mobile bottom bar overcrowds with ~8–10 icons. Alerts is a nav item routing to /notifications (apps/app/src/components/AppShell.tsx:202-208) instead of the expected top-right bell, and the ThemeToggle sits in the header rightSlot (AppShell.tsx:248) duplicating the one already in Profile settings (pages/profile/ProfilePage.tsx). This epic makes the chrome conventional: bell top-right, theme in settings, lean mobile bar with a drawer for the rest.

Scope: Frontend only. Reuses the existing notifications API from [[CasTyou Project Overview]] Epic 33 (unreadNotificationCount, notifications, markNotificationRead, markAllNotificationsRead) — no backend changes. All new/changed UI must come from @castyou/design-system (see [[Design System Rule]]).


DS-NAV-001 — NotificationBell dropdown component (design system)

  • [x] Done

Files:

  • Create: packages/design-system/src/components/layout/NotificationBell.tsx — bell button + unread badge + dropdown panel
  • Edit: packages/design-system/src/index.ts — export NotificationBell
  • Create: packages/design-system/src/components/layout/__tests__/NotificationBell.test.tsx

Component:

  • Presentational/headless: takes unreadCount: number, items: NotificationBellItem[], onOpen?, onItemClick(id), onMarkAllRead(), onSeeAll(). No data fetching inside the DS (the app wires the hooks).
  • Bell icon (Phosphor Bell, weight="fill" when panel open) with the same badge treatment as the current BellIcon (bg-brand-500, 99+ cap — port from apps/app/src/components/AppShell.tsx:46-57).
  • Click toggles a dropdown panel anchored top-right (click-outside to close — reuse the pattern from ProfileSwitcher in apps/app/src/components/AppShell.tsx:84-90). Panel lists recent notifications with unread dot, a Mark all read action, and a See all footer linking to the full page.
  • All surfaces use semantic theme tokens (bg-surface-card, border-ds-border, text-content-*) so it works in light/dark (see [[Design System Rule]] and DS-015).

Acceptance criteria:

  • Badge shows unread count (hidden at 0, 99+ cap); bell fills when open
  • Panel opens/closes on click and on click-outside; renders item list, mark-all-read, and see-all
  • Purely presentational (no GraphQL inside DS); looks correct in both themes
  • Tests cover: badge render, open/close, callbacks fire

FE-NAV-001 — Move alerts into the top-right header bell

  • [x] Done

Files:

  • Edit: apps/app/src/components/AppShell.tsx — remove the notifications entry from navItems (lines 202-208); add <NotificationBellConnected /> to the AppHeader rightSlot (left of / near ProfileSwitcher)
  • Create: apps/app/src/components/NotificationBellConnected.tsx — wires useUnreadCount() + useNotifications() (hooks/useNotifications.ts) into the DS NotificationBell; onItemClick marks read + navigates, onSeeAll/notifications, onMarkAllReadmarkAllNotificationsRead
  • Edit: apps/app/src/components/AdminShell.tsx — add the same bell to the admin header (admins lose the sidebar alerts item too)
  • Keep: pages/notifications/NotificationsPage.tsx and the /notifications route — it remains the "See all" destination

Notes:

  • The bell is the only alerts surface in the chrome now — no sidebar/bottom-bar alerts item on any breakpoint.
  • Local BellIcon helper in apps/app/src/components/AppShell.tsx is no longer needed there (its badge logic moves into the DS component) — remove if unused after the change.
  • Keep the existing 30s polling for unreadNotificationCount (already in useUnreadCount).

Acceptance criteria:

  • A bell with unread counter appears in the top-right header on every authenticated page (app + admin)
  • Clicking it opens the notifications dropdown; items mark-as-read and navigate; "See all" → /notifications
  • No "Alerts" item remains in the sidebar or mobile bottom bar
  • Unread count stays in sync with the page (mark-all-read clears the badge)

FE-NAV-002 — Remove ThemeToggle from the navbar (keep only in Profile settings)

  • [x] Done

Files:

  • Edit: apps/app/src/components/AppShell.tsx — remove <ThemeToggle /> from the header rightSlot (line 248)
  • Edit: apps/app/src/components/AdminShell.tsx — remove <ThemeToggle /> from the admin header (line ~164)
  • Keep: pages/profile/ProfilePage.tsx settings card — the toggle stays here as the single entry point

Notes:

  • ProfileSwitcher and CaStarsWidget stay in the header rightSlot; only the theme toggle leaves. The header bell from FE-NAV-001 joins this slot.
  • No change to ThemeProvider/useTheme (DS-015) — the persisted preference and system-preference fallback are untouched; we're only relocating the control.

Acceptance criteria:

  • Theme toggle no longer appears in the app or admin header
  • Theme toggle remains functional in Profile → Settings and persists as before
  • No regression to light/dark behavior on load

DS-NAV-002 — Responsive AppShell: full sidebar (desktop) + compact bottom bar & left drawer (mobile)

  • [x] Done

Files:

  • Edit: packages/design-system/src/components/layout/AppShell.tsx — split mobile vs desktop nav; add hamburger + drawer
  • Create: packages/design-system/src/components/layout/NavDrawer.tsx — left slide-in overlay rendering the full navItems (or fold into AppShell)
  • Edit: packages/design-system/src/index.ts — export NavDrawer if standalone
  • Edit: packages/design-system/src/components/layout/__tests__/AppShell.test.tsx (create if absent)

Behavior:

  • Desktop (md+): unchanged — the <aside> sidebar renders the full navItems list (AppShell.tsx:91-118).
  • Mobile (<md): the bottom bar (AppShell.tsx:129-143) renders only the primary items + a hamburger. Add a prop to mark the primary set:
    ts
    export interface AppShellProps {
      children: React.ReactNode;
      navItems: AppNavItem[];           // full list — desktop sidebar + mobile drawer
      primaryNavKeys?: string[];        // keys shown directly in the mobile bottom bar; default ['home','jobs','messages']
      header?: React.ReactNode;
      contentClassName?: string;
    }
    The bottom bar shows navItems.filter(i => primaryNavKeys.includes(i.key)) followed by a hamburger button (Phosphor List).
  • Hamburger → drawer: tapping it opens NavDrawer — a panel sliding in from the left over a dimmed backdrop, rendering the full navItems (same list/order as the desktop sidebar). Tapping a link navigates and closes the drawer; backdrop tap / Esc closes it. The drawer's active item highlighting matches the sidebar.
  • Reuse semantic tokens; animate the slide-in (200ms) and lock body scroll while open.

Notes:

  • The desktop sidebar and the mobile drawer render the same navItems, so there's one source of truth — only the bottom bar is filtered.
  • BottomTabBar (DS-009) remains the standalone component; this change is to the integrated AppShell nav that the app actually uses. Optionally reconcile/retire DS-009 if it stays unused.

Acceptance criteria:

  • Desktop: full link list visible in the sidebar (no behavior change)
  • Mobile bottom bar shows exactly Home, Jobs, Messages + a hamburger (no overcrowding)
  • Hamburger opens a left drawer with the full link list; selecting a link navigates and closes; backdrop/Esc closes; body scroll locked while open
  • Drawer link set/order matches the desktop sidebar; active highlighting consistent
  • Tests cover: bottom bar filters to primary keys + hamburger, drawer open/close, link click navigates & closes

FE-NAV-003 — Wire app AppShell to the responsive nav

  • [x] Done

Files:

  • Edit: apps/app/src/components/AppShell.tsx — pass primaryNavKeys={['home','jobs','messages']} to DSAppShell; confirm the full navItems (minus the removed alerts item) flows to both sidebar and drawer
  • Verify: apps/app/src/components/AdminShell.tsx adopts the same pattern if it uses its own shell (or document why admin stays sidebar-only)

Notes:

  • After FE-NAV-001 the navItems list no longer contains notifications; messages stays (it's a primary mobile item and keeps its unread badge).
  • Profile / Help / Report-bug / Applications / Talents / Pets / Admin all live in the desktop sidebar and the mobile drawer (not the bottom bar).

Acceptance criteria:

  • On a phone viewport, bottom bar = Home + Jobs + Messages + hamburger; everything else reachable via the drawer
  • On desktop, the sidebar is unchanged except the alerts item is gone (now the header bell)
  • Messages unread badge still shows on the bottom-bar item

TEST-NAV-001 — Tests for Epic 35

  • [x] Done

Frontend (Vitest/RTL):

  • NotificationBell.test.tsx — badge render/cap, open/close, mark-all-read & item callbacks
  • NotificationBellConnected — opening fetches/renders notifications; clicking item marks read + navigates; see-all routes to /notifications
  • AppShell.test.tsx (DS) — mobile bottom bar filtered to primaryNavKeys + hamburger; drawer opens/closes; drawer renders full list; link click navigates and closes
  • Regression: header no longer renders ThemeToggle; Profile settings still does; no notifications item in the nav list