Appearance
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):
- 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.
- Theme toggle leaves the navbar. The
ThemeToggleis removed from the header (and admin header); it lives only in Profile → Settings going forward.- 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— exportNotificationBell - 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 currentBellIcon(bg-brand-500,99+cap — port fromapps/app/src/components/AppShell.tsx:46-57). - Click toggles a dropdown panel anchored top-right (click-outside to close — reuse the pattern from
ProfileSwitcherinapps/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 thenotificationsentry fromnavItems(lines 202-208); add<NotificationBellConnected />to theAppHeaderrightSlot(left of / nearProfileSwitcher) - Create:
apps/app/src/components/NotificationBellConnected.tsx— wiresuseUnreadCount()+useNotifications()(hooks/useNotifications.ts) into the DSNotificationBell;onItemClickmarks read + navigates,onSeeAll→/notifications,onMarkAllRead→markAllNotificationsRead - 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.tsxand the/notificationsroute — 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
BellIconhelper inapps/app/src/components/AppShell.tsxis 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 inuseUnreadCount).
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 headerrightSlot(line 248) - Edit:
apps/app/src/components/AdminShell.tsx— remove<ThemeToggle />from the admin header (line ~164) - Keep:
pages/profile/ProfilePage.tsxsettings card — the toggle stays here as the single entry point
Notes:
ProfileSwitcherandCaStarsWidgetstay in the headerrightSlot; 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 fullnavItems(or fold into AppShell) - Edit:
packages/design-system/src/index.ts— exportNavDrawerif standalone - Edit:
packages/design-system/src/components/layout/__tests__/AppShell.test.tsx(create if absent)
Behavior:
- Desktop (md+): unchanged — the
<aside>sidebar renders the fullnavItemslist (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:tsThe bottom bar showsexport 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; }navItems.filter(i => primaryNavKeys.includes(i.key))followed by a hamburger button (PhosphorList). - Hamburger → drawer: tapping it opens
NavDrawer— a panel sliding in from the left over a dimmed backdrop, rendering the fullnavItems(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 integratedAppShellnav 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— passprimaryNavKeys={['home','jobs','messages']}toDSAppShell; confirm the fullnavItems(minus the removed alerts item) flows to both sidebar and drawer - Verify:
apps/app/src/components/AdminShell.tsxadopts the same pattern if it uses its own shell (or document why admin stays sidebar-only)
Notes:
- After FE-NAV-001 the
navItemslist no longer containsnotifications;messagesstays (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 callbacksNotificationBellConnected— opening fetches/renders notifications; clicking item marks read + navigates; see-all routes to/notificationsAppShell.test.tsx(DS) — mobile bottom bar filtered toprimaryNavKeys+ hamburger; drawer opens/closes; drawer renders full list; link click navigates and closes- Regression: header no longer renders
ThemeToggle; Profile settings still does; nonotificationsitem in the nav list