# UI Phase 2 — Shells & navigation — Report (2026-07-17)

## What was built

### Locale-aware navigation — one wrapper, both bugs fixed
`src/i18n/navigation.ts` wraps `createNavigation(routing)` (`Link`/`usePathname`/`useRouter`/
`redirect`/`getPathname`). **All chrome navigation now goes through it**: `SideBarNavItem`
renders via the wrapper's `Link` (unprefixed `href`, locale added automatically — one
navigation, no middleware 307 hop), `BottomBar` dropped its `withLocale` helper and `router.push`
via the wrapper, `NotificationBell` and `CustomerLayout`'s support/back buttons route via the
wrapper's `useRouter`. `grep -r "\$\{locale\}" src/layout` and the rewritten chrome components
now returns nothing. A new shared helper, `src/layout/matchActivePath.ts` (unit-tested,
longest-prefix winner-takes-all), backs both the sidebar and every bottom bar's active-item
detection — the sidebar's `pathname.startsWith(unprefixedPath)` bug (always-false against the
locale-prefixed `next/navigation` pathname) is gone; the active item now highlights on every
nurse/admin/partner route for the first time.

**Deliberately not touched:** `AppLink`'s `activeClassName` comparison (same underlying bug,
listed in the phase's suggested fixes). It has zero live consumers (`grep activeClassName` hits
only its own definition + test) and no CSS ever targets the `.active` class it would add —
purely inert. `AppLink` is also used for locale-prefixed navigation at ~10 non-chrome call sites
app-wide (`` `/${locale}${ROUTES.X}` ``); switching its internal `usePathname` to the locale-
stripped wrapper would flip that dead code from harmlessly-wrong to actively-wrong for those
sites without fixing anything visible. Left as-is; not a regression from this phase.

### Customer shell — contextual header + desktop treatment
`CustomerLayout` now renders a contextual `TopBar`: the brand lockup (`BrandLockup`, new — a
compact horizontal logo+wordmark reused in every sidebar shell's drawer header) on the 5 root
tabs, a page title + RTL-mirrored back chevron (`router.back()`) on pushed routes. Titles come
from `src/layout/routeTitle.tsx` — a static longest-prefix route→title map off the `nav`
namespace, plus a `PageTitleProvider`/`usePageTitleOverride` context slot for dynamic titles
area phases will wire in later (ships with static titles only, per scope). **Desktop decision
(≥`md`)**: the mobile `BottomBar` hides entirely in favor of an inline top-nav (`CustomerDesktopNav`,
MUI `Tabs`, same 5 items + `matchActivePath`) rendered as a second row inside the same fixed
`AppBar` (`TopBar` gained a `secondaryRow` slot for this). `BottomBar` also gained
`env(safe-area-inset-bottom)` padding for the iOS home-indicator overlap.

### Nurse shell — a workspace
`NurseLayout`'s sidebar is now sectioned (امروز / حرفهٔ من / مالی / پشتیبانی) via a new
`group` field on `LinkToPage` — `SideBarNavList` renders a `ListSubheader` whenever consecutive
items' `group` changes. The identity block is a real `ProfileSummary` card (new shared
component, `src/components/ProfileSummary/`): avatar, name (falls back to phone), masked phone
(`dir="ltr"`), and the nurse's own `TrustBadge` (`ownBadgeState(useVerificationStatus())`) —
skeleton while `/me` resolves, never an English "Current User"/"Loading...". Mobile gets a 5-tab
`BottomBar` (امروز/درخواست‌ها/ویزیت‌ها/درآمد/بیشتر); «بیشتر» opens the **same** sidebar drawer
(`TopBarAndSideBarLayout` exposes a `mobileBottomBar` render-prop that hands the drawer's
open-callback down — no second drawer). `ActorSwitcher` (new, dual-role only) sits under the
identity card.

### Admin + Partner shells — a dense console
`AdminLayout`'s sidebar is sectioned (اعتماد / مالی / پشتیبانی / سیستم) with **every
`useAdminCapabilities` gate preserved exactly** — grouping only changes presentation. Added the
missing `ROUTES.ADMIN_USERS` entry, gated on `caps.canManageRoles` like Roles. The
notifications sidebar item is gone, replaced by a header bell (`NotificationBell`'s `role` union
widened to include `'admin'`; `notificationsPath('admin')` → `ROUTES.ADMIN_NOTIFICATIONS` — both
minimal, pre-existing-seam extensions, not new endpoints). The TopBar carries a compact
`ProfileSummary` identity chip showing the admin's fine-grained role label (`admin.role_*`,
already existed). `PartnerLayout` gets the same TopBar identity slot showing the center's own
name (`useMyPartnerCenter`, skeleton while resolving) — the page-level access-denied state is
untouched.

### Public shell — stripped to a brand frame
`PublicLayout` no longer wraps `/login` in `TopBarAndSideBarLayout` at all: no hard-coded
`'Unauthorized - Balinyaar'` title, no pencil-icon drawer, no empty `BottomBar` strip. It's a
slim corner header (small `logo` icon + `LocaleSwitcher` + dark toggle) over the content —
deliberately not a second big brand lockup, since `AuthCard` already renders `BrandMark` inside
the login card. The dead `BOTTOM_BAR_DESKTOP_VISIBLE` flag and the commented-out anchor
alternates in `layout/config.ts` are gone.

### Cross-cutting engine fixes
- **SSR flash killed structurally, not patched.** `TopBarAndSideBarLayout` + `SideBar` no
  longer derive sidebar *structure* from `useIsMobile()`. `SideBar` renders **two Drawers over
  one content tree** — `variant="temporary"` (mobile) and `variant="permanent"` (desktop) —
  switched purely by `sx` breakpoint `display`. The permanent Drawer is a normal flex sibling of
  the main column (`TopBarAndSideBarLayout`'s content row is `direction="row"`), so desktop
  reserves its own width as part of native flex layout — first paint already has the sidebar,
  and RTL flexbox puts it at the reading-start side with **no manual offset math at all** (the
  old `paddingLeft/Right` keyed off `anchor.includes('left')` double-flip hack is deleted
  outright, not replaced with a logical-property equivalent — there was nothing left to offset).
  The mobile temporary Drawer's `anchor` is still physical (an MUI API constraint) but is now
  derived from `theme.direction` at render time, not a hardcoded per-breakpoint constant.
- **Drawer-close scoping fixed.** The close handler moved off the whole content `Stack` onto
  `SideBarNavList`'s `onClick` only — toggling dark mode, switching locale, or tapping a divider
  inside the mobile drawer no longer closes it; tapping a nav link does.
- **Chrome strings translated.** `'Open Sidebar'` → `common.open_sidebar`; the starter
  `'Logout Current User'` icon-only button is now a labeled, translated (`nav.logout`) full-width
  row in the sidebar footer, alongside the new `LocaleSwitcher`.
- **`TopBar` overflow fixed** (`noWrap` + `minWidth: 0` instead of manual `whiteSpace:'nowrap'`),
  starter comment residue deleted, and it gained `align`/`titleNode`/`secondaryRow` so one
  component now serves both the customer's centered brand header and the console shells'
  start-anchored breadcrumb-style title.
- Two small, justified `theme.ts` additions (the phase context's claim that `Drawer`/
  `BottomNavigation` were already restyled in phase 0 didn't hold — neither had overrides):
  `MuiDrawer` paper (background.default canvas + inset border) and `MuiBottomNavigationAction`
  (selected color + bold label weight), both off existing `--bal-*` tokens.

### Session affordances
- **Sign-out** now reachable in the customer shell for the first time: one labeled row
  (`profile.sign_out`) on the `/profile` hub, per the phase's explicit "one row, not a redesign"
  scope (full hub redesign deferred to phase 9). Sidebar shells keep it in the drawer footer.
- **`ActorSwitcher`** (new, `src/layout/components/`): renders nothing for a single-role
  session; for a dual customer+nurse session shows «نمای پرستار» in the nurse sidebar and
  «اپلیکیشن خانواده» on the customer profile hub, navigating only — `RoleGuard`/
  `resolveRoleDestination` remain the sole "which app" authority.
- **`LocaleSwitcher`** (new, `src/components/common/`): `router.replace(pathname, { locale })`
  via the wrapper, so it preserves the current route. In every sidebar footer, the customer
  profile hub, and the public shell. `DarkModeToggleButton`/`DarkModeFormSwitch` remain the only
  `useColorScheme()` subscribers — neither switcher adds a scheme subscription.

### `UserInfo` deletion
`src/components/UserInfo/` is deleted outright (no rename/shim). Its one call site
(`SideBar.tsx`) now renders the `identity` slot passed by the caller; the top-level `@/components`
barrel and `common/index.tsx` re-exports were swapped for `ProfileSummary`. No other file imported
it (`grep UserInfo` was clean before deletion beyond the component's own two files).

## What is now testable (and exactly how)

1. Nurse (`/nurse`): sidebar shows four labeled sections; the identity card shows name/phone/
   TrustBadge, not "Current User". Click «ویزیت‌ها» — it highlights, the TopBar shows the page
   title, one navigation in the Network tab (no 307).
2. Resize to mobile: nurse shell shows the 5-tab bar; «بیشتر» opens the drawer with the full
   grouped list + sign-out; toggling dark mode inside doesn't close it; a nav link does.
3. Customer `/`: brand lockup in the header. Open a nurse profile from search → header flips to
   title + back chevron; back returns to results; mirrors correctly on `/en`.
4. Customer ≥900px: no bottom tab bar; the inline top-nav is present and highlights the right tab
   on nested routes (e.g. a booking detail still lights up "Bookings").
5. `/profile`: sign-out row works; with a dual-role session, the actor switcher appears there and
   in the nurse sidebar.
6. Admin: TopBar shows page title + bell + role chip (e.g. «مالی» for finance); sidebar is
   sectioned and still capability-filtered — a finance-only admin sees no new items.
7. `/login` on `/fa`: no English text, no drawer, no bottom strip — just the corner logo +
   switchers + the `AuthCard`'s own brand mark.
8. Hard-reload `/nurse` on desktop: sidebar present at first paint (verified via `npm run build`
   not run here, but structurally guaranteed — see "SSR flash killed structurally" above).

## Verification performed this session

- `npm run check` (type + lint) — clean.
- `npm run test:ci` — 98 suites / 408 tests pass, including new tests for `matchActivePath`,
  `ProfileSummary`, `LocaleSwitcher`, `ActorSwitcher`.
- `npm run dev` + SSR smoke curls (via PowerShell, not Bash — this sandbox's Bash tool proxies
  loopback HTTP and returns a bare 502 regardless of the server's actual state):
  `/fa/login`, `/fa`, `/en`, `/fa/nurse`, `/fa/admin` all return 200 with no error-boundary/
  Next error markers in the HTML; `/fa/login` confirms `dir="rtl"`, the Mikhak font class, and
  the brand string, with the old `'Unauthorized - Balinyaar'` title gone (the one "Unauthorized"
  string left in the payload is Next's own internal App Router boundary metadata, unrelated).
- **Not performed**: a full four-locale × two-scheme × two-viewport visual pass in an actual
  browser (no browser/screenshot tool available in this session) — the structural/SSR checks
  above give high confidence, but visual polish (spacing, the desktop top-nav's exact look,
  dark-mode contrast on the new `ProfileSummary`/`ActorSwitcher`) has not been eyeballed. Flagging
  this explicitly per the "don't claim UI success you can't see" rule — recommend a human pass
  before merging, especially on the customer desktop treatment (the newest, least-precedented
  piece of this phase).

## What is mocked / waiting on a real service

None introduced — this phase is chrome over data that already flows (`useMe`, `useNurseProfile`,
`useVerificationStatus`, `useMyPartnerCenter`, `useUnreadCount`) behind existing `services/{domain}`
seams. No new mock/seam registered.

## Contracts

None produced or consumed. `/me` already returns `firstName`/`lastName`/`phone`, which was enough
for `ProfileSummary` — the anticipated gap ("`/me` lacking a display name") did not materialize,
so **no REQ was filed** (REQ-039+ still unused).

## Docs updated

- `client/CLAUDE.md` — Project Structure: `i18n/navigation.ts`, the full `layout/` tree (new
  `routeTitle.tsx`, `matchActivePath.ts`, `BrandLockup.tsx`, `ActorSwitcher.tsx`, the rewritten
  shell files), `components/ProfileSummary/`, `components/common/LocaleSwitcher/`.
- `.claude/skills/frontend-designer/SKILL.md` — §4 component table (`UserInfo` → `ProfileSummary`),
  §5 "Layout & page shells" rewritten for the four-shell architecture, the dual-Drawer engine, and
  `matchActivePath`.

## Follow-ups for later phases

- Full visual four-axes pass (see "Verification performed" above) — recommend before/alongside
  the next UI phase that touches customer screens.
- Dynamic per-page titles (nurse name, booking reference) via `usePageTitleOverride` — the slot
  exists; phases 4-6 and 9 (per the original scope note) should wire real titles in as they touch
  those screens.
- `AppLink`'s inert `activeClassName` bug (see "Deliberately not touched" above) — low priority,
  no known consumer; worth a real fix only if a future feature actually starts using it.

## Notable session correction

`RoleGuard.tsx` briefly had an unconditional `return children;` (added outside this phase, by the
user, for unrelated reasons) that broke `npm run check` — the dead code below it failed to
type-check because TypeScript doesn't apply control-flow narrowing inside unreachable code.
Confirmed via `git stash` that this predated my changes; the user removed the bypass mid-session.
`RoleGuard.tsx` was otherwise never touched by this phase, per scope.
