Files
baya-monorepo/dev/post-phase/ui/audit/cross-cutting-ux.md
T
2026-07-17 13:22:04 +03:30

75 lines
18 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.
# CROSS-CUTTING UX pattern audit — client/src (loading/empty/error states, tokens, RTL, responsiveness, a11y, motion, toasts, metadata, formatting)
## Current state
The client has a two-tier quality profile: feature surfaces built during the f0f15 phases are disciplined, while everything inherited from the MUI starter is untouched. Loading is a dual system — the starter `AppLoading` spinner (CircularProgress wrapper, 47 refs in 23 files) gates whole pages (customer home, checkout, auth splash), while MUI `Skeleton` is broadly adopted for list/detail loads (54 files). Nearly every data screen implements a real four-state branch (skeleton → error-with-retry → empty → data; ~95 `isError` refs across 55 files, ~124 retry/refetch refs), e.g. `CategoryGrid` in `(customer)/page.tsx:169-207`. Errors surface through a unified toast pipeline (`lib/toast/dispatchToast` → ToastBridge → notistack, used in 37 files including the fetch layer, brand-styled via tokens) plus inline `AppAlert`. Empty states are shared only in the backoffice (`components/admin/AdminEmptyState/AdminErrorState`); customer/nurse pages hand-roll the same dashed-border Paper 29 times across 23 files. There are zero Next.js `loading.tsx`/`error.tsx`/`not-found.tsx` files, the React `ErrorBoundary` is raw starter HTML with a stack trace, and the only `metadata` export in the whole app is the root layout's single static title.
Theming and internationalization are the strongest cross-cutting layers. Colors flow almost exclusively through `theme/tokens.css` custom properties (331 `var(--bal-*)` refs across 103 files; the only hard-coded hexes in .tsx are the starter PencilIcon SVG and tests), with complete light/dark schemes flipped by `data-mui-color-scheme`. RTL discipline is excellent: logical `start`/`end` everywhere, deliberate `dir="ltr"` islands for phone numbers, IBANs, OTP boxes, countdowns and coordinates (30+ sites), a direction-aware Emotion cache, and `PhoneNumberField` normalizing Persian/Arabic digits. Dates render Shamsi via `utils/date.ts` (`fa-IR-u-ca-persian`, 82 uses in 37 files) and numbers via `Intl.NumberFormat('fa-IR')` — though the `locale === 'fa' ? 'fa-IR' : 'en-US'` ternary is copy-pasted at 25+ call sites and there is no relative-time formatting anywhere. Responsive behavior relies on single-column flows and the mobile-first customer shell (TopBar + 5-tab BottomBar, content capped at 800px); explicit breakpoints appear only 12 times in 10 files and `useIsMobile` only in layouts. Motion is essentially absent (about 39 `transition` matches, almost all hover border-color; no keyframes, no reduced-motion handling). A11y is moderate: 47 `aria-` attributes in 28 of ~300 tsx files, with excellent pockets (OtpInput, NurseResultCard keyboard activation) and gaps (icon-only buttons named solely via Tooltip `title`). The nurse/admin/partner shells still run the starter `TopBarAndSideBarLayout` + `SideBar` + `UserInfo` chrome, `theme/theme.ts` defines no `components` overrides at all, and `AppButton` ships the starter's default `margin: 1` — visible as 213 `m: 0` workarounds across 82 files.
## Problems (19)
- **[high]** `client/src/components/common/ErrorBoundary.tsx` — The app-wide error boundary (wrapping every shell via TopBarAndSideBarLayout.tsx:104 and CustomerLayout.tsx:78) is raw starter UI: unstyled English '<h2>{name} - Something went wrong' plus the raw error.toString() and full componentStack inside a <details> — untranslated, unbranded, leaks internals to end users, and offers no retry/back affordance.
- evidence: lines 44-53: `<h2>{this.props.name} - Something went wrong</h2> ... {this.state?.errorInfo?.componentStack}`
- **[high]** `client/src/components/UserInfo/UserInfo.tsx` — The sidebar identity block is dead starter code: SideBar.tsx:56 renders `<UserInfo showAvatar />` with no user prop, so every nurse/admin/partner permanently sees the English literals 'Current User' and 'Loading...' in the drawer of the fa-default app.
- evidence: lines 34-36: `{fullName || 'Current User'}` / `{userPhoneOrEmail || 'Loading...'}`; prop typed `user?: any` and never supplied
- **[high]** `client/src/app/[locale]/layout.tsx` — The single metadata export in the entire app — every one of ~60 routes shares the title 'Balinyaar | بالین‌یار' and the placeholder description 'Balinyaar web application'; no generateMetadata, no per-page or per-locale titles, so browser tabs, history and share previews are indistinguishable.
- evidence: lines 54-58; grep for generateMetadata/<title> across client/src returns only this file
- **[high]** `client/src/components/common/AppIcon/config.ts` — The brand 'logo' icon is the starter's PencilIcon (a pencil is the logo in the nurse/admin top bar and auth), and the icon set mixes filled and outlined MUI weights (Star, CheckCircle, VerifiedUser, AccountCircle, Groups filled vs ~50 Outlined imports) — the direct source of the 'ugly icons' problem.
- evidence: line 115: `logo: PencilIcon,`; lines 4-32 filled imports vs lines 35-98 Outlined imports
- **[high]** `client/src/app/[locale]/(public-routes)/layout.tsx` — No route-level loading.tsx, error.tsx, not-found.tsx or global-error.tsx exists anywhere under client/src/app — unknown URLs render Next's default unbranded English 404, route transitions have no suspense fallback, and server-render failures show the default Next error screen (the public segment contains only /login).
- evidence: Glob client/src/app/**/{loading,error,not-found}.tsx → 'No files found'
- **[medium]** `client/src/components/common/AppButton/AppButton.tsx` — Starter default `margin: 1` on every button (DEFAULT_SX_VALUES) forces callers to write `sx={{ m: 0 }}` everywhere — 213 occurrences across 82 files — and passing any custom sx silently drops the default, making button spacing inconsistent by construction.
- evidence: lines 9-11 and 50: `sx: propSx = DEFAULT_SX_VALUES` where `DEFAULT_SX_VALUES = { margin: 1 }`
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — No shared user-facing EmptyState/ErrorState component: the dashed-border Paper pattern (`p: 3-4, textAlign: 'center', border: '1px dashed', borderColor: 'divider'`) is hand-rolled 29 times across 23 files (this file x2, search/results/page.tsx x2, nurse/visits/page.tsx, bookings/request/page.tsx x2, …) while admin got AdminEmptyState — all text-only, no icon/illustration, inconsistent copy and CTA presence.
- evidence: lines 176-195 vs identical blocks in search/results/page.tsx:79,115 — grep `border: '1px dashed'` → 29 hits in 23 files
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse dashboard — the landing screen for the entire nurse role — is still a PlaceholderScreen stub ('placeholder_body'), despite requests/visits/earnings/verification data all existing in services.
- evidence: line 7: `return <PlaceholderScreen icon="dashboard" title={t('dashboard')} description={tShell('placeholder_body')} />`
- **[medium]** `client/src/theme/theme.ts` — createTheme defines no `components` overrides at all — every MUI control (AppBar, Button, TextField, Chip, Tabs, Dialog) renders stock MUI apart from palette/radius/font, which is precisely why the app reads as a default-MUI starter rather than the calm warm brand.
- evidence: lines 19-36: theme = cssVariables + colorSchemes + typography + shape only
- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Nurse/admin/partner chrome is untouched starter: untranslated English tooltips in the fa-default app ('Open Sidebar' here, 'Logout Current User' in components/SideBar.tsx:76), physical paddingLeft/paddingRight keyed off anchor strings, and TopBar.tsx still carries the starter comment `// boxShadow: 'none', // Uncomment to hide shadow` with a centered nowrap title that can clip long Persian titles.
- evidence: line 71: `title={sidebarProps.open ? undefined : 'Open Sidebar'}`; TopBar.tsx:20,34
- **[medium]** `client/src/components/notifications/NotificationRow.tsx` — No relative-time formatting exists anywhere in the client (grep for ago/RelativeTimeFormat only hits a mock) — notifications, ticket inbox rows, and audit entries all show full absolute Shamsi timestamps, which is the wrong grain for inbox-style UIs ('۲ ساعت پیش' expected).
- evidence: grep `RelativeTime|timeago|ago` → only services/payouts/apis/mockApi.ts
- **[medium]** `client/src/components/NurseResultCard/NurseResultCard.tsx` — The `locale === 'fa' ? 'fa-IR' : 'en-US'` Intl ternary is duplicated at 25+ call sites (here lines 17, 39; CountdownTimer.tsx:84; booking/format.ts:11,21,45; earnings pages; checkout) instead of a shared helper next to utils/date.ts — and it has already drifted: admin/partners/[id]/page.tsx:121 passes the raw app locale ('fa') which resolves to Gregorian-locale digits differently than 'fa-IR'.
- evidence: grep `locale === 'fa' ? 'fa-IR'` → 25+ occurrences; admin/partners/[id]/page.tsx:121 uses `new Intl.NumberFormat(locale, …)`
- **[medium]** `client/src/components/common/AppIconButton/AppIconButton.tsx` — Icon-only buttons are named for AT solely via MUI Tooltip `title` (aria-describedby, not an accessible name) and disabled buttons drop the Tooltip entirely — combined with overall thin aria coverage (47 aria- attributes across 28 of ~300 tsx files), most icon buttons have no accessible name.
- evidence: lines 89-95: Tooltip wrap only when `title && !disabled`; no aria-label fallback
- **[low]** `client/src/components/config.ts` — Explicit responsive design is nearly absent outside the shells — 12 breakpoint usages in 10 files app-wide and useIsMobile only in layout/ — so desktop renders as a centered 800px phone column (CONTENT_MAX_WIDTH = 800) with no use of wider viewports on any customer or nurse screen.
- evidence: line 4: `export const CONTENT_MAX_WIDTH = 800`; grep `xs:|sm: |md: |breakpoints` in *.tsx → 12 hits
- **[low]** `client/src/app/globals.css` — Starter reset still sets `max-height: 100vh` on html/body plus blanket `overflow-x: hidden` — max-height on body serves no purpose and is a latent scroll/sticky bug; the shells then re-implement their own scroll containers around it.
- evidence: `html, body { max-width: 100vw; overflow-x: hidden; max-height: 100vh; }`
- **[low]** `client/src/hooks/layout.ts` — Starter mobile-detection module kept verbatim: three alternative hooks with commented-out variants, a body-classList mutation hook, and SSR always guessing mobile (SERVER_SIDE_MOBILE_FIRST = true), which makes desktop users see a one-frame mobile-first layout shift after hydration in the sidebar shells.
- evidence: lines 7-8, 39-48, 58-71, 76-79
- **[low]** `client/src/theme/typography.ts` — The EN brand display font (Space Grotesk) is referenced in the font stack but never loaded — the comment admits 'Not currently wired to a font loader' — so English headings silently fall back to system fonts; no type-scale tuning (sizes/line-heights) exists for Persian body text either.
- evidence: lines 3-5: `/** Space Grotesk … Not currently wired to a font loader */`
- **[low]** `client/src/components/common/AppIcon/AppIcon.tsx` — Unknown icon names log a console.warn in production and silently render the MoreHoriz starter 'default' icon — misspelled icon keys degrade invisibly (icon prop is typed `IconName | string`, so typos compile).
- evidence: lines 33-36: `console.warn(`AppIcon: icon "${iconName}" is not found!`)`
- **[low]** `client/src/layout/CustomerLayout.tsx` — No motion system anywhere: page/content transitions, list item entrances and skeleton→content swaps are all hard cuts (only ~39 transition matches app-wide, almost all hover border-color), and there is no prefers-reduced-motion handling to pair one with — the app feels static rather than calm.
- evidence: grep `keyframes|Fade|Grow|Collapse|animation` → 39 hits in 27 files, none page-level
## Opportunities (10)
- **One shared StateView system (empty / error / offline) for user-facing pages** (impact: high, effort: medium) — Promote the 29 hand-rolled dashed-Paper blocks into a single branded StateView component (icon or small warm illustration, title, body, optional CTA/retry), with the admin AdminEmptyState/AdminErrorState folded in as variants. Every list and detail page immediately gains consistent, warmer empty/error moments, and future screens get them for free.
- **Route-level chrome: loading.tsx skeleton shells, branded error.tsx and not-found.tsx, ErrorBoundary rewrite** (impact: high, effort: small) — Add per-route-group loading.tsx (skeleton of each shell), a Persian-first branded 404 with a way home, and error.tsx/global-error.tsx sharing one calm 'something went wrong' design with a retry button; rewrite components/common/ErrorBoundary.tsx to the same design (dev-only stack). Small files, disproportionate perceived-quality lift — these are the screens users hit at their most anxious moments in a healthcare product.
- **Brand pass via theme.components — kill the default-MUI look in one file** (impact: high, effort: medium) — Add component overrides to theme.ts: cream AppBar with teal text instead of the default filled bar, softer Paper/Card treatment, pill-ish buttons without AppButton's default margin (deleting 213 m:0 workarounds), branded TextField/Chip/Tabs focus and hover states, and terracotta reserved for the single accent. This is the highest-leverage restyle since zero overrides exist today — every screen changes at once.
- **Real nurse dashboard replacing the placeholder** (impact: high, effort: medium) — nurse/page.tsx should compose data that already exists in services/: today's visits with check-in CTA (f8), pending requests with countdown (f7), verification progress ring (f5), this-week earnings (f12), and profile-completeness nudges. The nurse role currently lands on a stub — the single biggest missing screen in the app.
- **Single-weight icon system + a real logomark** (impact: high, effort: medium) — Replace the mixed filled/outlined MUI set with one consistent weight (e.g. Material Symbols Rounded outlined, or a licensed healthcare set) mapped through the existing AppIcon registry — the abstraction makes the swap mechanical — and replace PencilIcon with an actual Balinyaar logomark used in TopBar, auth, favicon and the future 404/empty states.
- **Trust-forward nurse cards and profile hero** (impact: high, effort: medium) — Trust is the product: extend NurseResultCard and the public nurse profile beyond the single ✓ badge with credential chips (پروانه نظام پرستاری), completed-visit counts, years of experience, response-time, and a 'payments held in escrow' reassurance strip reusing EscrowNotice; add a 'how we verify' sheet linked from every badge. All data exists in search/verification services or is one field away.
- **Per-page titles + PageHeader unification** (impact: medium, effort: small) — Introduce a title template ('%s | بالین‌یار') with generateMetadata per route (localized), and generalize the admin-only AdminPageHeader into a shared PageHeader so customer/nurse pages stop hand-rolling h5/h1 blocks — fixing tab/history UX and heading consistency together.
- **Locale formatting helpers: formatNumber + relative time** (impact: medium, effort: small) — Add utils/number.ts (wrapping the copy-pasted `locale === 'fa' ? 'fa-IR' : 'en-US'` ternary) and a formatRelativeTime using Intl.RelativeTimeFormat('fa') for notifications, ticket inboxes and audit rows; migrate the 25+ inline call sites. Removes drift risk and makes inbox surfaces read naturally.
- **Public landing + public nurse profiles** (impact: high, effort: large) — The anonymous web surface is only /login today. A trust-first marketplace needs a public front door: hero with search, how-it-works (request → escrow payment → verified visit → weekly payout), category grid (reusing CategoryTile), verified-nurse counters, and indexable public nurse profiles — the acquisition and SEO surface the product currently lacks entirely.
- **Desktop-aware layouts + gentle motion pass** (impact: medium, effort: medium) — Above ~900px let key customer flows use the space (search results as list+detail or two-column checkout with a sticky order summary instead of the 800px phone column), and add one restrained motion layer (150200ms content fade/slide, skeleton crossfade) behind a prefers-reduced-motion guard to make the app feel calm rather than static.
## Keep (do not regress)
- Token discipline: essentially zero hard-coded colors in feature .tsx (only the starter PencilIcon SVG and tests); 331 var(--bal-*) references across 103 files, with complete, deliberate light and dark schemes in theme/tokens.css — dark mode flips cleanly via data-mui-color-scheme.
- RTL correctness as a habit: logical start/end and marginInline throughout, deliberate dir="ltr" islands for phone numbers, IBANs, OTP boxes, countdowns, ticket codes and map coordinates (30+ sites), direction-aware Emotion cache with stylis-plugin-rtl, and PhoneNumberField/PatientForm normalizing Persian/Arabic digit input.
- Shamsi-first formatting: utils/date.ts renders fa-IR-u-ca-persian via Intl (82 uses across 37 files), money utils centralize IRR→Toman with fa-IR digits, and tabular-nums + dir=ltr is applied where numbers sit in RTL text.
- The four-state data pattern (skeleton → error-with-retry → empty → data) is genuinely implemented across feature pages (e.g. CategoryGrid in (customer)/page.tsx:169-207) — ~95 isError branches and ~124 retry/refetch affordances; state coverage needs restyling, not rebuilding.
- Unified toast pipeline: dispatchToast → ToastBridge → notistack, callable from non-React fetch code, brand-styled via tokens in NotistackProvider, used consistently across 37 files.
- AdminDataTable: RTL-safe align='inherit', horizontal scroll inside its own container so wide worklists never break the page, typed column renderers, aria-label support.
- TrustBadge's honest three-state design (verified/unverified/expired, 'never a hard-coded hex', unverified deliberately non-alarming) and the verified-only search invariant surfaced on every result card.
- Customer shell fundamentals: mobile-first TopBar + 5-tab BottomBar with locale-aware longest-prefix active-tab matching, reading-width content column, and support/notification affordances in the header.
- OtpInput: exemplary a11y/RTL work — dir=ltr group with role=group, per-box aria-labels, paste splitting, and automatic focus advance; NurseResultCard is keyboard-activatable with a visible focus ring.
- Mikhak font strategy (fa-only attachment, preload:false so /en never downloads it) and the root-layout locale/dir/color-scheme wiring with its documented reasoning.