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

297 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](ui-phase-0-design-language.md),
> [Phase 1](ui-phase-1-primitives-and-states.md) · **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](../../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](ui-phase-0-design-language.md):** 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](ui-phase-1-primitives-and-states.md):** 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](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](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](../../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](audit/shell-and-navigation.md)): 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](../../phases/_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 (`UserInfo` →
`ProfileSummary`) 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.