# Shared component primitives + icon system (client/src/components/common/, components/config.ts, components/index.tsx, PlaceholderScreen, and the primitives pages are forced to hand-roll)

## Current state

The `client/src/components/common/` layer is eight wrappers inherited nearly verbatim from the karpolan react-mui starter: AppButton, AppIconButton, AppIcon (+ its `config.ts` string registry, `icons/PencilIcon.tsx`, `utils.ts`), AppLink (AppLinkNextNavigation.tsx), AppAlert, AppImage, AppLoading, and a class-based ErrorBoundary. Their defaults live as module constants in `components/config.ts` (APP_BUTTON_VARIANT='contained', APP_ALERT_SEVERITY='error', APP_ICON_SIZE=24, CONTENT_MAX_WIDTH=800) rather than in the theme — and `theme/theme.ts` has no `components` overrides at all, so everything MUI renders default-MUI. The wrappers carry starter DNA: AppButton ships a `margin: 1` on-all-sides default plus `label`/`text` duplicate props and spreads a `underline` prop onto non-link buttons; AppIconButton wraps its output in a useMemo keyed on a fresh `restOfProps` object; AppImage is entirely unused in product code; ErrorBoundary's fallback is raw English `<h2>{name} - Something went wrong</h2>` plus a componentStack dump, and it is the app's ONLY error surface — there is no error.tsx/not-found.tsx/loading.tsx anywhere in the App Router tree.

The icon system is `<AppIcon icon="name">` resolving through a ~85-entry lowercase registry (`common/AppIcon/config.ts`). Feature phases added coherent snake_case domain names (check_in, post_surgery, escrow-adjacent earnings/refunds/moderation icons — all MUI `*Outlined`), but they sit next to the starter's filled set (Home, Dashboard, EventNote, Groups, PeopleAlt, AccountBalanceWallet, MedicalServices, CheckCircle, Cancel, Star, Info, Settings, AdminPanelSettings) plus eight dead starter entries (daynight/night/day/visibilityon/visibilityoff/signup/login/settings — zero usages). Two structural defects: (1) AppIcon passes `size` as SVG width/height *attributes*, which MUI SvgIcon's class CSS (`width:'1em'; height:'1em'`, SvgIcon.js:45) overrides — so every `size={14..48}` request across 40+ callsites silently renders at 24px; only the one custom SVG respects size, and that SVG is (2) the starter's Twemoji *pencil* with hard-coded cartoon fills (#EA596E, #FFCC4D…), registered as `logo` and used as the brand mark in the top bar and auth splash.

Above `common/`, `components/index.tsx` exports ~35 domain composites (StatusChip, TrustBadge, PriceDisplay, OtpInput, DocumentUpload, NurseResultCard…), several of which are genuinely well built (token-driven `--bal-*` colors, i18n-agnostic contracts, BigInt-safe money). But the reusable *page* primitives were built only for the backoffice — AdminPageHeader, AdminEmptyState, AdminErrorState, ConfirmDialog, AdminDataTable, AdminPager all live in `components/admin/` and are used nowhere else — so 40+ customer/nurse pages hand-roll the same header (47 `component="h1"` occurrences), 24 files hand-roll dashed-border empty states, 12 page files assemble 57 raw MUI `<Dialog>` confirm flows, 81 files place 139 ad-hoc `<Paper>`s, 19 files hand-concatenate `formatIrrToToman(x) + t('currency_toman')`, and 17 files new up their own `Intl.NumberFormat/DateTimeFormat`. Loading language is split between bare AppLoading spinners (~18 screens) and per-page improvised Skeleton stacks.

## Problems (16)

- **[high]** `client/src/components/common/AppIcon/AppIcon.tsx` — The `size` prop is silently broken for every MUI-registry icon: size is passed as SVG width/height attributes (propsToRender, lines 38-46), but MUI SvgIcon's emotion class sets `width:'1em'; height:'1em'` (node_modules/@mui/material/SvgIcon/SvgIcon.js:45) and class CSS beats presentation attributes — so all ~40 callsites requesting 14-56px (PlaceholderScreen size={48}, SelectRole size={28}, VisitNoteCard size={14}, TicketInboxScreen size={36}) render at the default 24px. Icon hierarchy across the whole app is flattened to one size; only the custom PencilIcon actually scales.
  - evidence: AppIcon.tsx:38-46 `propsToRender = { height: size, ... size, width: size }`; SvgIcon.js:45 `width: '1em', height: '1em'` in the styled root
- **[high]** `client/src/components/common/AppIcon/icons/PencilIcon.tsx` — The brand mark of a trust-first nursing marketplace is the starter's Twemoji cartoon pencil with hard-coded fills (#D99E82 #EA596E #FFCC4D #292F33 #CCD6DD #99AAB5) that ignore the color prop. It is registered as `logo` and rendered in the top bar (TopBarAndSideBarLayout.tsx:70) and on the auth splash (auth/BrandMark.tsx:21, which passes color="var(--bal-primary)" to no effect). First-impression trust surface = a writing-tool emoji in off-brand colors.
  - evidence: PencilIcon.tsx:8-24 hard-coded Twemoji palette; BrandMark.tsx:21 `<AppIcon icon="logo" size={56} color="var(--bal-primary)" />`
- **[high]** `client/src/components/common/ErrorBoundary.tsx` — The app's only crash UI is the starter ErrorBoundary: an unstyled, untranslated, LTR English `<h2>{name} - Something went wrong</h2>` plus a `<details>` block that dumps error.toString() and the full React componentStack to end users. No retry affordance, no brand styling, and it's what a Persian-speaking family sees if anything throws. There are also zero error.tsx / not-found.tsx / loading.tsx files in the entire App Router tree (glob over client/src/app returns nothing), so route-level failures and 404s fall through to framework defaults.
  - evidence: ErrorBoundary.tsx:44-53 renders `<h2>… Something went wrong</h2>` + `{this.state?.errorInfo?.componentStack}`; mounted at layout/TopBarAndSideBarLayout.tsx:104 and layout/CustomerLayout.tsx:78
- **[high]** `client/src/components/common/AppButton/AppButton.tsx` — Starter default `margin: 1` on all sides (DEFAULT_SX_VALUES, lines 9-11) fights every real layout: 63+ callsites across 38 files pass `sx={{ m: 0 }}` just to neutralize it, and because the default only applies when NO sx is given, any button that passes other sx silently loses the margin — outer button spacing is therefore inconsistent app-wide. Default `color='inherit'` (line 45) instead of primary also forces `color="primary"` boilerplate on every CTA.
  - evidence: grep `<AppButton ... sx={{ m: 0` → 63 occurrences / 38 files (e.g. patients/page.tsx:97, admin/ConfirmDialog.tsx:96,105); 238 total AppButton usages
- **[medium]** `client/src/components/common/AppIcon/config.ts` — The registry mixes three visual generations: ~13 filled starter icons (Home, Dashboard, EventNote, Groups, PeopleAlt, AccountBalanceWallet, MedicalServices, Settings, Info, AdminPanelSettings, CheckCircle, Cancel, Star) against ~55 `*Outlined` feature icons — nav rails and status chips read as an incoherent default-MUI grab bag (this is the 'ugly icons' the owner senses). It also still registers eight dead starter entries with zero usages: daynight, night, day, visibilityon, visibilityoff, signup, login, settings — violating the repo's own no-dead-code rule.
  - evidence: config.ts:5-33 filled imports vs :35-98 Outlined imports; usage grep: daynight/night/day/visibilityon/visibilityoff/signup/login/settings each = 0 hits outside the registry
- **[medium]** `client/src/components/common/AppIcon/config.ts` — Missing icons for a full app: there is no back/forward/chevron-start navigation arrow anywhere (grep for ArrowBack|ChevronLeft|ChevronRight|KeyboardArrow across client/src = 0 matches), so detail pages (booking, nurse profile, request tracker) cannot render an RTL-flippable back affordance; also absent: share, copy, receipt/invoice, help/FAQ, kebab (more-vert) for card action menus, attach, phone (only `emergency`=LocalPhone), and a plain non-circled check. `expand` (ExpandMore) is the sole directional glyph.
  - evidence: grep ArrowBack|ChevronLeft|ChevronRight|arrow_back|KeyboardArrow → 'No matches found'; only 1 file in the app uses router.back()
- **[medium]** `client/src/components/UserInfo/UserInfo.tsx` — Pure starter leftover shipped into the product shell: `user?: any` prop, hard-coded English fallbacks 'Current User', 'Loading...', 'User Avatar', and name/email fields that don't match the phone-OTP identity model — and SideBar renders `<UserInfo showAvatar />` with NO user prop at all, so every authenticated sidebar permanently shows an empty avatar with English 'Current User / Loading...' in the fa-default app.
  - evidence: UserInfo.tsx:6 `user?: any`, :34 `'Current User'`, :36 `'Loading...'`; layout/components/SideBar.tsx:56 `<UserInfo showAvatar />` (plus :76 hardcoded English tooltip 'Logout Current User')
- **[medium]** `client/src/components/admin/ConfirmDialog.tsx` — The four page-level primitives that exist — ConfirmDialog, AdminPageHeader, AdminEmptyState, AdminErrorState — are exiled under components/admin/ and used only there, so customer/nurse pages hand-roll the identical patterns: patients/page.tsx builds its own confirm Dialog + title header + skeleton + dashed empty state; addresses/page.tsx has 7 raw <Dialog> usages, nurse/coverage 4, nurse/services/MyServicesList 4, bookings/request/[id] 4; 47 hand-rolled `variant="h5" component="h1"` headers across 44 files; 30 `border: '1px dashed'` empty-states across 24 files.
  - evidence: grep `<Dialog` under app/ → 57 occurrences /12 files; grep `component="h1"` → 47/44 files; patients/page.tsx:87-101 (header), 103-108 (skeleton), 110-120 (dashed empty state)
- **[medium]** `client/src/utils/money.ts` — Excellent BigInt money utils exist but there is no `<Money>` display primitive, so 19 files hand-assemble `{formatIrrToToman(x)} {tc('currency_toman')}` and 17 files construct their own `Intl.NumberFormat/DateTimeFormat` — money rendering (grouping, Persian digits, signed negative styling on earnings/refunds) and date rendering are re-decided per page on the most trust-sensitive surfaces (checkout, invoice, earnings, refund status). PriceDisplay only covers catalog unit-rates.
  - evidence: grep currency_toman → 24 occurrences / 19 files (checkout/page.tsx, invoice/page.tsx, EarningsBalanceHeader…); grep Intl.(NumberFormat|DateTimeFormat) → 26 / 17 files outside utils
- **[medium]** `client/src/components/config.ts` — Starter config as app-wide defaults: a bare `<AppAlert>` renders a FILLED ERROR alert (APP_ALERT_SEVERITY='error'), CONTENT_MAX_WIDTH=800 with the nonsensical starter comment 'CONTENT_MIN_WIDTH = 320 // CONTENT_MAX_WIDTH - Sidebar width'. These component defaults belong in `createTheme({ components })` — and theme/theme.ts (lines 20-35) defines NO components overrides at all, which is exactly why the whole app renders as default MUI.
  - evidence: components/config.ts:10 `APP_ALERT_SEVERITY = 'error'`, :4-5 starter comment; theme/theme.ts:20-35 createTheme with no `components` key
- **[low]** `client/src/components/common/AppButton/AppButton.tsx` — Starter prop cruft: duplicate `label`/`text` props ('Alternate to .text'), a `// Missing props` comment block, jsdoc claiming a 'Box around to specify margins' that doesn't exist, and line 85 `{...{ ...restOfProps, underline }}` spreads `underline="none"` onto plain (non-link) MUI Buttons as an invalid DOM attribute.
  - evidence: AppButton.tsx:16-19 label/text + ':19 // Missing props'; :28 jsdoc 'with Box around'; :85 underline spread
- **[low]** `client/src/components/common/AppIcon/AppIcon.tsx` — Unknown icon names `console.warn` in production and silently render MoreHoriz ('…') — a wrong icon name in a trust surface degrades to an ellipsis nobody notices; the invalid `size` attribute is also spread onto the DOM <svg>, and the documented `title` tooltip does nothing (title attribute on inline SVG is not a tooltip; MUI wants `titleAccess`).
  - evidence: AppIcon.tsx:33-35 warn + `ICONS.default` fallback; :8-13 Props documents `title` as hover hint
- **[low]** `client/src/components/common/AppImage/AppImage.tsx` — Dead starter component: zero product usages (only its own test imports it) yet still exported from the common barrel; hard-codes `unoptimized={true}` citing a 'custom loader' that doesn't exist, defaults 256x256, English `alt='Image'` fallback.
  - evidence: grep <AppImage → matches only AppImage.test.tsx; AppImage.tsx:19 comment 'Uses custom loader + unoptimized'
- **[low]** `client/src/components/common/AppLoading/AppLoading.tsx` — The only shared loading primitive is a bare centered CircularProgress ('3rem', starter constant) used as the full-page loading state on ~18 screens, while other screens improvise their own Skeleton stacks (e.g. patients/page.tsx `[0,1].map(<Skeleton variant="rounded" height={96}>)`) — two competing loading languages and no reusable ListSkeleton/CardSkeleton/DetailSkeleton.
  - evidence: AppLoading used in 18 page files (search, checkout, bnpl, profile…); 40+ files import MUI Skeleton directly with per-page layouts
- **[low]** `client/src/components/common/AppIconButton/AppIconButton.tsx` — Cargo-cult useMemo wraps the rendered IconButton keyed on `restOfProps` — a new object every render — so it never memoizes anything; alpha() hover hack for non-MUI colors is starter residue.
  - evidence: AppIconButton.tsx:62-85 useMemo deps include restOfProps
- **[low]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse's post-login home is still a PlaceholderScreen ('dashboard' icon + generic placeholder body) — for the supply side of the marketplace, the landing screen is literally the empty-state scaffold; admin/users and admin/notifications are also placeholders.
  - evidence: nurse/page.tsx:7 `return <PlaceholderScreen icon="dashboard" ...>`; grep PlaceholderScreen under app/ → 4 files

## Opportunities (10)

- **One-file icon-system swap to a single coherent family** (impact: high, effort: medium) — The string-registry indirection means the entire app's iconography can be replaced by editing only AppIcon/config.ts: pick ONE family (all MUI *Rounded for warmth, or an inlined open set like Phosphor/Solar with softer strokes that suits 'clinical-but-human'), map all 85 names to it, add the missing names (back — RTL-flippable via a wrapper that rotates in rtl, receipt, copy, share, help, kebab, phone, plain check), delete the 8 dead starter entries, and type IconName strictly so unknown names fail at compile time instead of console.warn + '…'.
- **Fix AppIcon sizing via fontSize, not attributes** (impact: high, effort: small) — Change AppIcon to drive MUI icons with `style={{ fontSize: size }}` (or sx) instead of width/height attributes. This single fix restores the intended 14-56px hierarchy at every existing callsite simultaneously — the highest leverage-per-line change available in the codebase.
- **Real Balinyaar brand mark** (impact: high, effort: medium) — Replace the Twemoji pencil: design a simple symbol (e.g. a home + pulse/leaf motif in deep teal with a cream counter, terracotta only as micro-accent) as a currentColor SVG so BrandMark's `color="var(--bal-primary)"` actually works, and use it in the top bar, auth splash, favicon, and the future email/invoice header. For a trust-first healthcare product the mark is a functional trust cue, not decoration.
- **Promote the admin primitives + build the missing shared kit** (impact: high, effort: large) — Move ConfirmDialog, PageHeader, EmptyState, ErrorState out of components/admin into common/ and add the primitives pages provably keep hand-rolling: Section/AppCard (one Paper recipe — 139 ad-hoc Papers today), ListSkeleton/CardSkeleton, Money (signed coloring, Persian digits, Toman label — 19 hand-rolled sites), DateText (Shamsi via existing utils/date.ts), DescriptionList (61 hand-rolled label/value rows), StatCard, FormSection, BackLink. Then sweep pages onto them. This is the difference between 'restyled starter' and 'design system'.
- **Theme components layer instead of wrapper constants** (impact: high, effort: medium) — Add a `components` section to createTheme: MuiButton (defaultProps color='primary', disableElevation, no default margin — retire DEFAULT_SX_VALUES and the 63 `m:0` patches), MuiAlert (standard severity semantics, soft brand-tinted backgrounds), MuiPaper (outlined-by-default with --bal tokens), MuiChip radius, MuiTextField shape, focus-visible rings in teal. Wrappers then shrink to genuine additions (icon-by-name, link composition) instead of re-defaulting MUI per instance.
- **Branded error / 404 / loading route files** (impact: high, effort: medium) — Add locale-aware error.tsx, global-error.tsx, not-found.tsx and per-shell loading.tsx with calm Persian copy, the brand mark, and a retry CTA; give ErrorBoundary the same visual fallback and stop printing componentStack to users (log it instead). Crash surfaces are trust surfaces in healthcare.
- **Nurse home dashboard (currently a placeholder)** (impact: high, effort: large) — Replace the nurse PlaceholderScreen with a real home: today's visits with check-in shortcuts, pending request countdowns, earnings snapshot (reusing EarningsBalanceHeader), verification/credential-expiry nudges, and unread support tickets. The supply side currently lands on an empty scaffold every login.
- **Richer trust presentation built on TrustBadge** (impact: high, effort: medium) — TrustBadge is a small chip; trust is the product. Add a VerificationPanel primitive for nurse profile/search detail: what was verified (identity, license, Shahkar), when, by whom, with an expandable 'how Balinyaar verifies' explainer — turning the existing honest badge state into a persuasive, inspectable trust story.
- **Warm empty-state illustration set** (impact: medium, effort: medium) — Replace the 24 dashed-border Paper empty states with a shared EmptyState primitive that accepts a small branded SVG illustration (cream/teal line style, terracotta accent) per domain — patients, addresses, bookings, earnings, search-no-results — moving the tone from 'unconfigured dashboard' to 'calm, human product'.
- **Fix the sidebar identity block** (impact: medium, effort: small) — Replace starter UserInfo with a typed ProfileSummary fed by the /me query (display name, masked phone with Persian digits, role label, TrustBadge for nurses) — the current always-'Loading...' English block undermines every authenticated screen.

## Keep (do not regress)

- The AppIcon string-registry indirection itself (`<AppIcon icon="verified">` + one config.ts) — it concentrates the entire icon system into a single swap point, and the snake_case domain names (check_in, post_surgery, escrow-era earnings/refunds/moderation) are well-chosen and consistently used.
- StatusChip and TrustBadge (components/StatusChip, components/TrustBadge): fully token-driven via --bal-* semantic vars (auto dark-scheme), data-status/data-badge-state test hooks, and TrustBadge's honest-by-construction states (verified only when aggregate approved; expired visually distinct from never-verified, unverified deliberately non-alarming) — the right foundation for trust UI.
- utils/money.ts + PriceDisplay discipline: BigInt integer-safe Rial↔Toman, Persian digit formatting, totals only ever price×count at the field boundary — never regress this on any new Money primitive.
- components/admin/ConfirmDialog's interaction contract: required-reason gating, loading state that disables both buttons and prevents double-submit, caller-owns-the-mutation separation — promote it, don't rewrite it.
- The 'already-translated props' presentational contract (PlaceholderScreen, AdminPageHeader, StepperHeader document it explicitly) keeping primitives i18n-agnostic, and their RTL-safe logical flex layouts with no directional CSS.
- booking/format.ts and utils/date.ts: locale-aware clocks and Shamsi dates via Intl (fa-IR-u-ca-persian) with no date library, with honest null returns for open check-ins.
- AppLink's Next.js+MUI composition: external links auto-get target=_blank + rel='noopener noreferrer', internal links go through NextLink, active-class support — works and is RTL-neutral.
- The consistent `@/components` barrel import pattern — every page already imports primitives from one place, which makes the coming design-system sweep mechanical.
