# 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 f0–f15 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 (150–200ms 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.
