Files
baya-monorepo/archive/post-phase/ui/audit/component-primitives.md
T
2026-08-02 18:48:32 +03:30

69 lines
20 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.
# 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.