# 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.