cleanup phases 6
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user