Files
baya-monorepo/dev/post-phase/ui/ui-phase-2-shells-and-navigation.md
T
2026-07-17 13:22:04 +03:30

23 KiB
Raw Blame History

UI Phase 2 — Shells & navigation

Mission: replace the starter chrome with per-actor shells — consumer-app chrome for customers, a workspace for nurses, a dense console for admin/partner — and fix the navigation-correctness defects the audit verified: the sidebar active state that never fires, the middleware redirect hop on every sidebar click, the fragile RTL anchoring, and the desktop SSR mobile-first flash. Phases 01 gave the app a design language and primitives; this phase is where every actor finally gets chrome that looks like Balinyaar and navigates correctly. This phase owns client/src/layout/.

Track: frontend · Depends on: Phase 0, Phase 1 · Unlocks: every actor gets branded, correct chrome — the area redesigns (phases 311) compose inside these shells Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the frontend-designer skill — both are mandatory.

1. Context — where this sits

The feature layer is disciplined, but the chrome around it is the untouched react-starter-kit. Diagnosed current state (all verified in code):

  1. The sidebar active highlight never fires. SideBarNavItem.tsx:28 compares pathname.startsWith(path) where pathname (from next/navigation) is locale-prefixed (/fa/nurse/requests) and path is unprefixed (/nurse/requests) — defineRouting uses localePrefix: 'always', so the comparison always fails. Nurse/admin/partner users get zero "where am I" signal. AppLink's activeClassName (AppLinkNextNavigation.tsx:95, pathname == currentPath) has the same bug.
  2. Three locale strategies coexist in the chrome. Sidebar links push unprefixed hrefs through raw next/link (SideBarNavItem.tsx:34 via AppLink) — a middleware redirect hop on every click and a locale-flip risk for /en users; BottomBar.tsx:14 and NotificationBell manually prefix with /${locale}; CustomerLayout.tsx:52 does the same inline. There is no createNavigation wrapper — src/i18n/ holds only routing.ts + request.ts.
  3. The chrome is starter junk on a trust-first product. PublicLayout.tsx:9 titles the login screen 'Unauthorized - Balinyaar' in English; SideBar.tsx:56 renders <UserInfo showAvatar /> with no user — an eternal English "Current User" / "Loading..." (UserInfo.tsx:34-36, prop typed user?: any); TopBarAndSideBarLayout.tsx:71 tooltips 'Open Sidebar'; SideBar.tsx:76 says 'Logout Current User'.
  4. Structural defects: SERVER_SIDE_MOBILE_FIRST = true (hooks/layout.ts:8) makes every desktop SSR paint the mobile shell, then content jumps 240px after hydration; content offset uses physical paddingLeft/Right keyed to anchor strings (TopBarAndSideBarLayout.tsx:53-60) — RTL correctness by double-flip coincidence; the drawer-close handler sits on the whole content Stack (SideBar.tsx:52) so any tap inside closes it; BottomBar has no safe-area padding and renders unconditionally on desktop (CustomerLayout.tsx:81); the customer shell has no sign-out, no back affordance, no contextual title; no shell has a locale switcher or an actor switcher for dual-role sessions.

What already exists (do not rebuild):

  • Phase 0: the brand mark (registered as logo), the theme.components pass (AppBar/Drawer/ListItemButton/BottomNavigation already restyled), the single icon family incl. a direction-aware back chevron, elevation/motion/focus tokens.
  • Phase 1: PageHeader, state views, route-level loading.tsx/error.tsx/not-found.tsx, per-route metadata, formatting utils.
  • RoleGuard + role hydration (refinement phase 2): RoleGuard wraps all four shells with resolved-vs-pending /me hydration, error recovery, and mismatch redirects. Chrome only here — never touch it.
  • The per-actor shell split: CustomerLayout / NurseLayout / AdminLayout / PartnerLayout mapped 1:1 to route groups. Restyle and restructure the chrome inside this architecture; keep the split.
  • BottomBar's longest-prefix active matching (BottomBar.tsx:31-41) — the one nav component that matches correctly today. Keep its semantics; generalize them.
  • Performance-conscious composition: DarkModeToggleButton/DarkModeFormSwitch are the only useColorScheme subscribers; NotificationBell isolates the polling unread count. Shells never re-render on theme flips or bell updates — preserve this.
  • AdminLayout's capability-gated nav via useAdminCapabilities() (AdminLayout.tsx:21-37).

2. Required reading (do this first)

  • audit/shell-and-navigation.md — the full 20-problem inventory with file/line evidence and the keep-list. This is your problem spec.
  • audit/cross-cutting-ux.md — the chrome-adjacent items (English chrome strings, UserInfo, hooks/layout.ts, metadata) and its keep-list.
  • The frontend-designer skill (.claude/skills/frontend-designer/SKILL.md) — the design contract; phase 0 will have updated it with the new tokens/icons.
  • Code, in this order: client/src/layout/ (all files — you own this folder), client/src/i18n/routing.ts, client/src/components/common/AppLink/, client/src/components/UserInfo/UserInfo.tsx (you will delete it), client/src/components/auth/RoleGuard.tsx (do not touch — know why), client/src/hooks/layout.ts, client/src/hooks/auth.ts (useActorRole, useAdminCapabilities), client/src/context/auth/types.ts (SessionUser.roles — feeds the actor switcher), client/src/constants/routes.ts, client/src/components/TrustBadge/, client/src/services/profiles/ + client/src/services/auth/hooks/useMe.ts (feed the identity card), client/src/components/notifications/NotificationBell.tsx.
  • client/CLAUDE.md — "Golden rules", "Direction (RTL/LTR)", the theme system, and the Project Structure layout section you must update at close.
  • next-intl v4 docs for createNavigation (routing-aware Link/usePathname/useRouter/redirect).

3. Scope — build this

3.1 Locale-aware navigation — one wrapper, two bugs fixed

Create src/i18n/navigation.ts: createNavigation(routing) from next-intl/navigation, exporting Link, usePathname, useRouter, redirect, getPathname. Then route all chrome navigation through it:

  • SideBarNavItem / SideBarNavList: link via the new Link; compute active state against the new usePathname() (which strips the locale prefix, so unprefixed ROUTES.* compare directly). This makes the active highlight fire for the first time and removes the middleware redirect hop + locale-flip risk in one move.
  • Active matching must be longest-prefix winner-takes-all (the BottomBar semantics): extract a small shared helper (e.g. src/layout/matchActivePath.ts, unit-tested) used by both the sidebar and the bottom bars — plain startsWith would keep /nurse (dashboard) lit on every nurse route.
  • BottomBar: drop the manual withLocale prefixing in favor of the wrapper's router; keep its matching via the shared helper.
  • AppLink's activeClassName comparison, NotificationBell's router.push, and CustomerLayout's inline `/${locale}${…}` all migrate to the wrapper. After this, grep -r '/${locale}' src/ inside chrome code should return nothing.

3.2 Customer shell — contextual header + deliberate desktop

Rework CustomerLayout:

  • Contextual header. On the 5 root tabs (/, /bookings, /patients, /wallet, /profile — the (customer) group has no URL segment): the phase-0 brand lockup (mark + wordmark). On pushed routes (nurse profile, booking detail, checkout, ticket thread…): page title + back chevron (the phase-0 auto-mirrored icon) that calls router.back(). Drive it with a route→title map in src/layout/ (longest-prefix over ROUTES.*, titles from the existing nav/shell namespaces) plus a lightweight per-page override slot (React context) for dynamic titles — the area phases (46, 9) will feed nurse/booking names into it later; ship static titles now. Kill the static «اپلیکیشن خانواده» label (tShell('customer_app') today).
  • Keep the support entry, NotificationBell, and dark toggle in the header (badge/popover upgrades belong to phase 10 — leave slots, don't build them).
  • BottomBar: add paddingBottom: 'env(safe-area-inset-bottom)' on the Paper (the home-indicator overlap is on the primary mobile nav); refine the active state on top of the phase-0 BottomNavigation override (selected color + label weight — tokens, not hexes).
  • Desktop treatment — decide and implement deliberately. Recommended: a constrained app frame — the content column keeps CONTENT_MAX_WIDTH, gains side gutters on a background.default canvas — and above the md breakpoint hide the mobile tab bar in favor of a top-nav variant (the same 5 items as inline header tabs). Implement with CSS breakpoints (sx display keys), not useIsMobile branching (see 3.6).

3.3 Nurse shell — a workspace, not a starter drawer

Rework NurseLayout (still on the shared engine, which you are also refitting in 3.6):

  • Grouped sidebar with subheaders + dividers, replacing the flat 10-item array (NurseLayout.tsx:19-33): امروز (dashboard, requests, visits) · حرفهٔ من (services, coverage, verification) · مالی (earnings, bank) · پشتیبانی (support). Extend the nav-item model with a group key; the engine renders ListSubheader-style sections. Group labels are i18n keys in both catalogs.
  • A real identity card: new typed ProfileSummary shared component (src/components/ProfileSummary/, co-located test): avatar, display name, masked phone in Persian digits, role label, and TrustBadge when the actor is a nurse. Feed it from the /me session (useMe — phone, roles) plus the profiles domain for name/avatar where hydrated; render graceful skeleton/fallback states — never English literals. DELETE src/components/UserInfo/ (the starter user?: any card) and every import/test of it.
  • Mobile: a 5-tab nurse bottom nav so field nurses stop digging through a drawer: امروز (dashboard) · درخواست‌ها · ویزیت‌ها · درآمد · بیشتر — «بیشتر» opens the drawer with the remaining items (profile, services, coverage, bank, verification, support, sign-out). Reuse BottomBar.
  • TopBar shows the current page title via the same route→title engine as 3.2 (kill the static «نمای پرستار»).

3.4 Admin + partner shells — a dense console

  • Slim top bar: current page title (route→title map) as a start-anchored breadcrumb-style label — not the centered static console name — plus a bell (widen NotificationBell's role union to include 'admin'; the /admin/notifications center already exists — note this minimal foundation extension in your report) and an identity chip (ProfileSummary compact variant or a chip: name/phone + the fine-grained role label off roleCodes — a finance admin should see they're finance).
  • Sectioned sidebar with the now-working active state: اعتماد (verification, reviews) · مالی (payouts; refunds are worked via tickets — do not invent a refunds nav item) · پشتیبانی (tickets, alerts) · سیستم (config, holidays, audit, roles, partners, users). Add the missing /admin/users entry (ROUTES.ADMIN_USERS exists; gate it like roles on caps.canManageRoles — display convenience, server is the authority). The notifications sidebar item is replaced by the header bell. Keep every useAdminCapabilities gate exactly as is — grouping must not change what a role sees.
  • Partner shell: same engine + its own 4-item nav; identity area shows the center name from useMyPartnerCenter (fallback skeleton while resolving — the page-level access-denied handling stays where it is).

3.5 Public shell — strip it to a brand frame

PublicLayout today wraps login in starter dashboard chrome: the hard-coded English 'Unauthorized - Balinyaar' title (PublicLayout.tsx:9), a pencil button opening a drawer containing only a dark-mode switch, and an empty BottomBar strip on mobile (PublicLayout.tsx:34, BOTTOM_BAR_ITEMS = []). Replace it with a minimal centered brand shell: the phase-0 logo, a locale switcher, and the dark toggle — no sidebar, no bottom bar, no TopBarAndSideBarLayout. Delete the dead BOTTOM_BAR_DESKTOP_VISIBLE flag from layout/config.ts with it. (The login screen itself — hero, trust presence — is DEFERRED → phase 3.)

3.6 Cross-cutting engine fixes (TopBarAndSideBarLayout + SideBar + TopBar)

  • Drawer close scoping: move the close handler off the content Stack (SideBar.tsx:52) onto the nav links themselves — toggling dark mode or mis-tapping a divider must not close the drawer.
  • Kill the desktop SSR flash: stop deriving shell structure from useIsMobile (SERVER_SIDE_MOBILE_FIRST renders the mobile shell, then jumps 240px). Render responsively with CSS: breakpoint-keyed sx values (paddings, drawer variant/visibility via display) so the desktop first paint already includes the persistent sidebar. hooks/layout.ts stays for non-structural consumers; the shells stop depending on it for layout.
  • Logical properties: replace the physical paddingLeft/Right + anchor?.includes('left') logic (TopBarAndSideBarLayout.tsx:53-60, 89-91) with paddingInlineStart / start-anchored drawer semantics — RTL correctness by construction, not by the stylis double-flip coincidence.
  • Chrome strings → both catalogs: 'Open Sidebar' (TopBarAndSideBarLayout.tsx:71), 'Logout Current User' (SideBar.tsx:76); the UserInfo literals die with the component. Zero English chrome remains on /fa.
  • TopBar: fix the whiteSpace: 'nowrap' overflow risk (ellipsis + minWidth: 0); delete the starter comment residue (TopBar.tsx:20); title alignment becomes start-anchored for the console shells per 3.4.
  • layout/config.ts: delete the commented-out anchor alternates (lines 8-9) and dead flags; keep the constants authoritative — update them, never bypass them.

3.7 Session affordances — sign-out, actor switch, locale switch

  • Sign-out reachable in EVERY shell. The customer shell currently has none — add a sign-out row to the profile-tab hub (/profile) now (the full hub redesign is DEFERRED → phase 9; one labeled row, not a redesign). Sidebar shells keep sign-out in the drawer footer — now labeled and translated.
  • Actor switcher for dual-role sessions: SessionUser.roles already lives in AuthContext. When a session holds both customer and nurse, show «نمای پرستار ⇄ اپلیکیشن خانواده» in the nurse sidebar and on the customer profile hub. Navigation only — RoleGuard and resolveRoleDestination stay the "which app" authority.
  • Locale switcher (fa/en) in all shells (sidebar footer / customer profile hub / public shell): switch locale preserving the current path via the 3.1 wrapper (router.replace(pathname, { locale })).
  • Keep DarkModeToggleButton/DarkModeFormSwitch as the only useColorScheme subscribers — the switchers must not add scheme subscriptions to the shells.

4. Mocks & seams in this phase

None introduced. This phase is chrome over data that already flows (useMe, profiles, useMyPartnerCenter, the notifications unread count) behind the existing services/{domain} seams — UI stays mock-tolerant regardless of each domain's mock flag. REQ posture: if a backend gap surfaces (the likely one: /me lacking a display name for ProfileSummary, forcing a second profile fetch per shell), append a REQ to ../../shared-working-context/frontend/requests/for-backend.md — REQ-001…038 are taken; number from REQ-039. Never edit server/.

5. Critical rules you must not get wrong

  1. RoleGuard and role/redirect logic untouched. Refinement phase 2 built resolved-vs-pending hydration; this phase is chrome, not security or routing policy. The actor switcher navigates; it never re-derives roles.
  2. useAdminCapabilities gating stays exactly as is — sectioning the admin nav must not add, remove, or loosen a single capability gate.
  3. Do not regress the keep-lists (audit): the root [locale] layout (lang/dir, conditional Mikhak, cookie-seeded scheme, RTL Emotion cache) is untouchable; the per-actor shell split stays; the customer 5-tab IA (Home/Bookings/Patients/Wallet/Profile) stays; longest-prefix active matching stays; DarkModeButton remains the sole useColorScheme subscriber; NotificationBell keeps isolating the poll; CONTENT_MAX_WIDTH reading column stays for text-heavy views; ErrorBoundary keeps wrapping every shell's main content.
  4. Design contract non-negotiables: every new string in both en.json/fa.json (fa is the product's voice — write it first); tokens/palette keys, never hexes; logical/RTL-safe props only (this phase exists partly to remove physical ones — do not add new ones); verify dark mode on every surface you touch; MUI v9 API only (no useFlexGap/flexWrap as Stack props); new shared components (ProfileSummary, the active-path helper, any header context) get co-located tests.
  5. Fetch/cookies/provider rules untouched: no raw fetch, no document.cookie, no layout above [locale], no createTheme() in components. ProfileSummary consumes existing hooks — it does not add API calls of its own design.
  6. Deleting UserInfo is a removal, not a rename — check src/**/*.test.{ts,tsx} and the @/components barrel for imports, and update the frontend-designer skill's component table (it lists UserInfo) to point at ProfileSummary.

6. Definition of Done

On top of the shared definition-of-done.md:

  • npm run check green; npm run test:ci green (new tests for ProfileSummary, the active-path helper, and any touched shared component); en.json/fa.json in sync.
  • src/i18n/navigation.ts exists and all chrome navigation flows through it — no raw next/link and no manual /${locale} prefixing left in src/layout/ or chrome components.
  • The sidebar highlights the active item on every nurse/admin/partner route (first time ever), and clicking a sidebar link produces one navigation in the Network tab — no 307 middleware hop, no locale flip on /en.
  • Customer shell: brand lockup on the 5 root tabs; title + mirrored back chevron on pushed routes; safe-area padding on the bottom bar; on ≥md the mobile tab bar is hidden in favor of the desktop treatment.
  • Nurse shell: grouped sidebar (امروز/حرفهٔ من/مالی/پشتیبانی), ProfileSummary identity card with TrustBadge, 5-tab mobile bottom nav; UserInfo deleted repo-wide.
  • Admin/partner shells: sectioned capability-gated sidebar, page-title top bar, bell + identity chip (admin), center-name identity (partner).
  • Public shell: no English title, no pencil, no empty drawer or bottom strip — logo + locale switcher + dark toggle only.
  • Desktop first paint of a sidebar shell includes the persistent sidebar — no 240px post-hydration jump (verify with a hard reload, network throttled).
  • Sign-out is reachable in all four shells; a dual-role session sees the actor switcher; every shell has a locale switcher that preserves the current path.
  • Visual verification on the four axes — /fa + /en × light + dark — and mobile + desktop for every shell (fa first).

7. How to test (what a human can verify after this phase)

  1. Log in as the seeded nurse → /nurse. The sidebar shows four labeled groups and your name/phone/TrustBadge — not "Current User". Click «ویزیت‌ها»: the item highlights, the top bar reads the page title, and the Network tab shows a single navigation (no 307).
  2. Resize to mobile (or open devtools device mode): the nurse shell shows a 5-tab bottom nav; tab «بیشتر» opens the drawer; toggling dark mode inside the drawer does not close it; tapping a nav link does.
  3. As a customer on /: the header shows the brand lockup. Open a nurse profile from search → the header flips to title + back chevron; the chevron returns to results. On /en the chevron mirrors correctly.
  4. On desktop ≥900px as a customer: no mobile tab bar pinned to the bottom; the desktop nav variant is present; content sits in the framed column.
  5. Go to /profile as a customer: a sign-out row exists and works. With a dual customer+nurse session, the actor switcher appears here and in the nurse sidebar, and lands on the other shell (RoleGuard permitting).
  6. Log in as the seeded admin: top bar shows the page title, the bell, and your role chip (e.g. «مالی» for a finance admin); the sidebar is sectioned and still shows only capability-permitted consoles. As the finance-only admin, confirm no new items appeared.
  7. Open /login logged-out on /fa: no English anywhere, no drawer, no bottom strip — brand mark, locale switcher, dark toggle.
  8. Hard-reload /nurse on desktop: the sidebar is present at first paint; no sideways content jump.
  9. Switch locale from any shell's switcher on a deep route (e.g. /fa/nurse/earnings): you land on /en/nurse/earnings, same page.

8. Hand off & document (close the phase)

  • Update client/CLAUDE.md → Project Structure: the layout/ section (new shell composition, route→title map, removed starter engine parts), i18n/navigation.ts, components/ProfileSummary/, and the UserInfo deletion. Update the frontend-designer skill's §4 component table (UserInfoProfileSummary) and §5 layout description if shell variants changed.
  • Write the report at dev/shared-working-context/reports/ui-phase-2-report.md: what changed per shell, the navigation-wrapper migration list, the desktop-treatment decision you made, any foundation files you extended minimally (e.g. NotificationBell role union), and screenshots/notes from the four-axes check.
  • List any REQs filed (REQ-039+) with one-line rationales; "none" is an acceptable outcome.
  • Save a memory note per operating-rules §8: the shells are now the branded per-actor chrome, chrome navigation is createNavigation-based (active state + no redirect hop), UserInfo is gone, and phases 311 must route new chrome strings/titles through the route→title map rather than static labels.