Files
baya-monorepo/dev/shared-working-context/reports/ui-phase-2-report.md
T
2026-07-17 19:05:17 +03:30

14 KiB
Raw Blame History

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 LinkToPageSideBarNavList 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 treevariant="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 (UserInfoProfileSummary), §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.