# Shared feature components (client/src/components/* excluding common/, admin/, auth/, booking/, geography/, messaging/, notifications/) ## Current state This layer is ~34 single-purpose presentational components, one folder each (component + index barrel + test), re-exported from client/src/components/index.tsx. Authorship is remarkably uniform: typed FunctionComponent, a JSDoc header explaining the domain rule the component encodes, caller-owned or namespace-scoped i18n (never hard-coded strings, except one component), data-* attributes for tests, and display-only money/dates through shared utils (formatIrrToToman/parseIrr BigInt, formatShamsiDate). The visual language that exists is: flat Paper (elevation={0}, 1px 'divider' border, borderRadius 2), a borderInlineStart accent stripe keyed to a semantic token for stateful panels (BankStatusPanel, EarningsBalanceHeader, PayoutHistoryRow failure, DocumentUpload error/rejected), and MUI Chip-based badges (StatusChip, TrustBadge, PaymentStatusBadge) whose colors come exclusively from the --bal-* CSS custom properties in src/theme/tokens.css (both light and dark schemes defined). RTL is handled with logical properties (borderInlineStart, marginInlineStart:'auto', textAlign 'start'/'end') plus deliberate dir="ltr" islands for IBANs, phone numbers, OTP boxes, transfer references, and the countdown clock. Contrary to the "no design pass" expectation, this layer is NOT starter-grade — it was clearly written during the f0–f15 feature phases with real domain intent (honest refund/BNPL copy, escrow trust notice, negative-balance "owed back" framing, no-delete variant rows). The one true starter fossil is UserInfo (any-typed props, English 'Current User'/'Loading...' fallbacks, rendered in the sidebar for every logged-in user). The real weaknesses are systemic rather than per-component: (1) the card/row/badge anatomy is repeated by convention, not shared — the same Paper recipe is hand-rolled ~12 times with padding drifting across p:1.5/2/2.5/3 and accent stripes at 3px vs 4px, and the label/value row is re-implemented three times (SummaryRow in BookingRequestSummaryCard, MetaLine in PayoutHistoryRow, inline rows in PriceBreakdown); (2) the trust-critical surfaces are the flattest — TrustBadge is pixel-identical in anatomy and color to a generic StatusChip 'verified', rating stars are colored with the dark-ochre alert token over a near-invisible 14%-alpha empty state, and NurseResultCard carries only name/rating/distance/price; (3) terracotta (--bal-secondary #d98c6a) has quietly become the default "money text" color across six components and fails WCAG contrast on white; (4) selection states speak five different visual dialects (filled chip vs border-only vs border+tint vs terracotta vs default ToggleButton); and (5) everything composes stock MUI Material icons and raw MUI Stepper, so despite the token discipline the rendered result still reads default-MUI. Theme.ts has no component-level overrides, so any anatomy not written in sx falls back to MUI defaults. ## Problems (13) - **[high]** `client/src/components/UserInfo/UserInfo.tsx` — Untouched starter scaffolding shipped in the authenticated sidebar: `user?: any` prop, hard-coded English fallbacks 'Current User' and 'Loading...' in a fa-default product, email-based fallback display in a phone-OTP product, and a 3rem glyph inside a 64px avatar. Violates the repo's own 'no starter scaffolding / no dead template code' rule and is the first thing every logged-in user sees (rendered by src/layout/components/SideBar.tsx:56). - evidence: lines 6 (`user?: any`), 34 (`{fullName || 'Current User'}`), 36 (`{userPhoneOrEmail || 'Loading...'}`) - **[high]** `client/src/components/PriceBreakdown/PriceBreakdown.tsx` — Terracotta used as small-text color fails WCAG contrast in light mode on the most trust-critical numbers. `--bal-secondary` #d98c6a on white ≈ 2.7:1 (AA needs 4.5:1 at these sizes) is the grand-total color here and also in RefundStatusCard.tsx:89 (refunded amount), BnplPlanCard.tsx:68 (monthly amount), InstallmentScheduleRow.tsx:55 (down payment), CancellationPolicyDisclosure.tsx:55 (fee line); `--bal-secondary-dark` #bf6f4d caption in BnplPlanCard.tsx:62 ≈ 3.8:1 also fails. This simultaneously breaks the 'terracotta as a SINGLE sparing accent' brand rule — it is now the default money color across 6+ components. - evidence: line 62: `sx={{ fontWeight: 800, color: 'var(--bal-secondary)' }}` on the total amount - **[high]** `client/src/components/RatingInput/RatingInput.tsx` — The trust-critical star rating renders badly three ways: filled stars use `var(--bal-warning)` — a dark-ochre alert-background token (#8a6418 light / #97701f dark) — so 'gold' stars read muddy brown; empty stars use `var(--bal-divider)` (a 14%-alpha rgba) and are near-invisible on white paper and dark surfaces alike; and fill is integer-only (`n <= value`), so fractional averages can't render — the nurse profile works around it with Math.round, displaying a 4.5 average as a perfect 5-star row (search/nurse/[nurseId]/page.tsx:260), overstating ratings on the platform's core trust surface. NurseResultCard.tsx:87 and BookingRequestSummaryCard.tsx:86 use the same ochre star. - evidence: line 53: `const color = n <= value ? 'var(--bal-warning)' : 'var(--bal-divider)';` - **[medium]** `client/src/components/TrustBadge/TrustBadge.tsx` — The platform's core trust mark has no distinct visual identity: it is anatomically identical to StatusChip (same small filled MUI Chip, same 16px icon, same `--bal-success` background that StatusChip uses for generic 'active'/'verified' states — compare StatusChip.tsx:16-17). A verified-identity healthcare credential and an 'active service variant' chip are indistinguishable at a glance, and there is no affordance to see WHAT was verified (identity, license, background check). - evidence: lines 18 + 39-46: verified = `{ bg: 'var(--bal-success)', ... }` rendered as a plain `` - **[medium]** `client/src/components/NurseResultCard/NurseResultCard.tsx` — The search decision point is information-thin for a trust-first marketplace: no service/variant name (each result row IS a bookable variant), no nurse gender indicator (same-gender matching is a load-bearing product rule carried in the query), no experience/completed-bookings count, no review snippet — just avatar, name, one badge, rating, optional distance, and price. Families are choosing an in-home caregiver off four data points. - evidence: lines 77-112 render only name + TrustBadge + rating/count + distance + price_from - **[medium]** `client/src/components/RelationSelect/RelationSelect.tsx` — Selection states are inconsistent across the five choice controls in this layer, and this one is the weakest: selected relation cards get only a 2px `primary.main` border — no background tint, no check glyph, no hover/pressed feedback — while ConditionChips/ReviewTagSelector use a filled primary chip, CategoryTile uses border+`--bal-primary-soft` tint, BnplPlanCard uses 2px terracotta border+tint, and GenderToggle falls back to the default gray MUI ToggleButton selected state. The role="radio" group also lacks roving tabindex/arrow-key navigation (every card is tabIndex={0}). - evidence: lines 53-55: `border: '2px solid', borderColor: selected ? 'primary.main' : 'divider'` is the entire selected treatment - **[medium]** `client/src/components/StepperHeader/StepperHeader.tsx` — A bare default-MUI Stepper wrap (numbered circles, default connector — theme.ts defines no component overrides), doing double duty as wizard progress (onboarding/verification) AND as a refund status timeline inside RefundStatusCard.tsx:76-79. A status tracker rendered as a form-wizard control, with no timestamps and the stock MUI look the owner is trying to escape. - evidence: lines 20-28: raw `` with zero styling - **[medium]** `client/src/components/OtpInput/OtpInput.tsx` — Missing `autoComplete="one-time-code"` on the digit inputs, so iOS/Android SMS code autofill never triggers — on the product's ONLY login path, in a market where OTP login is the norm. (PhoneNumberField.tsx similarly omits `autoComplete="tel"`.) The multi-box pattern itself also fights autofill; a single hidden input with visual boxes would receive the OS-suggested code. - evidence: lines 121-128: `htmlInput: { inputMode: 'numeric', maxLength: 1, ... }` — no autoComplete - **[medium]** `client/src/components/EarningsRow/EarningsRow.tsx` — Shared card anatomy exists only by convention: the `Paper elevation={0} / 1px divider / borderRadius 2` recipe is hand-rolled here and in ~11 sibling components with drifting padding (p:1.5 InstallmentScheduleRow, p:2 PatientCard/VariantCard/VisitNoteCard, p:2.5 here/PriceBreakdown/PayoutHistoryRow, p:3 EarningsBalanceHeader) and accent stripes at 3px (EarningsBalanceHeader.tsx:97, PayoutHistoryRow.tsx:91) vs 4px (BankStatusPanel.tsx:67, EarningsBalanceHeader.tsx:55, DocumentUpload.tsx:164); the label/value row is re-implemented three times (SummaryRow, MetaLine, PriceBreakdown rows). No shared Card/Row primitive means every future restyle is a 12-file change and drift is inevitable. - evidence: line 57: `sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}` — the 12th copy of this literal - **[low]** `client/src/components/BnplPlanCard/BnplPlanCard.tsx` — Selecting a plan changes borderWidth 1→2, shifting the card contents by 1px (RelationSelect avoids this with a constant 2px border); and the card shows only monthly amount + fee% + down-payment% with no total-cost-of-credit line, so plans with different terms are not honestly comparable. - evidence: lines 49-50: `borderColor: selected ? ... , borderWidth: selected ? 2 : 1` - **[low]** `client/src/components/CountdownTimer/CountdownTimer.tsx` — Uses the 'pending' hourglass glyph as a clock (a 'schedule' clock icon exists in the registry), hard-codes fontSize '1.5rem' outside the type scale, and 'urgent' mode only swaps teal→terracotta — no progress ring/bar or intensifying treatment for the payment-deadline window it was built for. - evidence: lines 66 + 100-104: `urgent ? 'var(--bal-secondary)' : ...` and `` beside `fontSize: '1.5rem'` - **[low]** `client/src/components/EarningsRow/EarningsRow.tsx` — The negative commission row is fed through the generic PriceBreakdown with no deduction treatment — same weight/color as positive rows, relying entirely on Intl fa-IR minus-sign placement inside an RTL paragraph; a deduction should read as one (parentheses, muted/error tone, or an explicit 'کسر' prefix). - evidence: lines 48-51: `String(-parseIrr(item.balinyaarCommissionIrr))` passed as a plain PriceBreakdown row - **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — Loading skeletons are generic rectangles that don't match the card anatomy they replace (bare `Skeleton height={112}` for NurseResultCard rows) — a pattern repeated across pages because no card in this layer ships a skeleton twin; content jumps when avatars/chips/price rows pop in. - evidence: line 75: `` ## Opportunities (12) - **Extract the shared card/row/badge kit the layer already implies** (impact: high, effort: medium) — Codify the de-facto anatomy into 4-5 primitives and migrate the ~12 hand-rollers: `SurfaceCard` (flat Paper, one padding scale sm/md/lg, radius token), `AccentCard` (SurfaceCard + semantic borderInlineStart stripe at one width), `LabelValueRow` (replaces SummaryRow/MetaLine/PriceBreakdown rows, with an ltr-value option for refs/IBANs), `SelectableCard` (one selected language: constant border + soft tint + check glyph, used by RelationSelect/CategoryTile/BnplPlanCard/GenderToggle), and `MoneyText` (amount + currency + optional deduction styling, correct contrast). Every future brand pass then touches one file per primitive instead of twelve. - **Design a real trust system around TrustBadge** (impact: high, effort: medium) — Trust is the product: give the verified mark a proprietary shape (e.g. a teal shield/seal distinct from status chips), and make it expandable — tapping opens a bottom sheet listing what Balinyaar verified (identity ✓, nursing license ✓, background check ✓, IBAN ownership ✓) with dates, fed by the existing verification-status query. Reuse the same sheet on NurseResultCard, the nurse profile, and booking summary. This converts an ambient chip into an explorable trust artifact — the single highest-leverage design move available. - **NurseResultCard v2 — the decision card** (impact: high, effort: medium) — Add the variant/service name (the row is a variant), a gender indicator (load-bearing for matching), completed-visits count, and a one-line top review tag (e.g. «منظم و دقیق» from the review vocabulary); make the avatar larger and photo-forward with the trust seal overlapping it. Ship a `NurseResultCardSkeleton` twin. Consider a compact/comfortable density prop so the same card serves list and map views later. - **Dedicated rating tokens + fractional stars** (impact: high, effort: small) — Add `--bal-rating` / `--bal-rating-empty` token pairs (a warm amber-gold with a visible empty outline in both schemes) and give RatingInput fractional fill (clip-path or dual-layer) so a 4.5 average renders honestly instead of being rounded to 5. Swap NurseResultCard/BookingRequestSummaryCard star colors to the same token. Small change, visible on every trust surface. - **Retire terracotta as the money color** (impact: high, effort: small) — Define a `--bal-money-emphasis` token (ink/teal-dark in light, lifted cream in dark) for totals and amounts, reserving terracotta for the one primary financial CTA per screen (pay button, selected BNPL plan) per the brand's 'single sparing accent' rule. Fixes the contrast failures in PriceBreakdown/RefundStatusCard/BnplPlanCard/InstallmentScheduleRow/CancellationPolicyDisclosure in one pass. - **Swap the icon registry to a coherent humane set** (impact: medium, effort: medium) — All glyphs are stock MUI Material icons, which is a big contributor to the default-MUI feel. Because AppIcon centralizes the registry (common/AppIcon/config.ts), replacing Material with a single warm stroke set (Lucide/Phosphor/Solar, 1.5-2px stroke, rounded caps) is a one-file change that instantly re-skins all 34 feature components — including replacing the leftover Twemoji PencilIcon 'logo'. - **StatusTimeline component for refunds (and future booking states)** (impact: medium, effort: small) — Replace StepperHeader inside RefundStatusCard with a purpose-built vertical timeline: step label + timestamp + channel note per node, teal completed nodes, animated 'in transit' node. Reusable for booking lifecycle and verification progress; leaves StepperHeader to actual wizards. - **Answer 'when do I get paid?' in EarningsBalanceHeader** (impact: medium, effort: small) — The header shows four buckets but not the one thing nurses actually ask: the next weekly payout date (server-derivable from the batch schedule + holiday shift). Add a 'برداشت بعدی' line with the Shamsi date under the net balance, and optionally a mini trend of recent payouts. - **OTP autofill + single-input architecture** (impact: medium, effort: small) — Add autoComplete="one-time-code" now (one line), then refactor OtpInput to one hidden input driving visual digit boxes so OS keyboard code-suggestion works reliably; add autoComplete="tel" to PhoneNumberField. Directly reduces login friction for every user. - **Skeleton twins co-located with cards** (impact: medium, effort: small) — Ship `` statics for NurseResultCard, EarningsRow, PayoutHistoryRow, PatientCard, VisitNoteCard matching their exact anatomy (avatar disc, chip row, price line), so pages stop hand-rolling height-guessed rectangles and loading feels designed. - **Total-cost honesty line on BnplPlanCard** (impact: medium, effort: small) — Add a served 'total you will pay' line (down payment + n×installment) so interest-free vs fee-bearing plans are comparable at a glance — an honest-lending pattern consistent with the platform's existing BNPL-honesty rules (RefundEtaBanner already models this tone). - **Rewrite or delete UserInfo** (impact: medium, effort: small) — Replace the starter UserInfo with a branded profile block: initials avatar off --bal-primary-soft (matching NurseResultCard/BookingRequestSummaryCard), i18n'd fallbacks, masked phone via the existing maskIranMobile util, and the user's role chip — or fold it into the redesigned sidebar entirely. ## Keep (do not regress) - Token discipline is exemplary and must not regress: zero hard-coded hexes across all 34 feature components (verified by grep — only the starter PencilIcon in common/ has literals); every color resolves from --bal-* semantic tokens defined for both light and dark schemes, with in-code comments enforcing the rule (StatusChip.tsx:13-14, TrustBadge.tsx:14-16). - RTL discipline: logical properties throughout (borderInlineStart accent stripes, marginInlineStart:'auto' in VisitNoteCard, textAlign 'start'/'end') plus deliberate dir="ltr" islands for IBANs (BankStatusPanel:96, PayoutHistoryRow:71-79), phone/OTP digits, transfer references, and the HH:MM:SS countdown clock (CountdownTimer:103) — a grep for marginLeft/textAlign:'left' finds nothing. - Money invariants: all amounts are served IRR digit-strings formatted through the BigInt-safe money util; PriceBreakdown's dev-mode reconciliation guard (rows must sum to the total or console.error) catches upstream data bugs; no component ever computes money except the sanctioned price×sessionCount estimate in PriceDisplay. - Honest trust copy encoded as components: EscrowNotice's product-mandated verbatim fa escrow message, RefundEtaBanner's BNPL ~7-10-business-day honesty, RefundStatusCard suppressing ALL success framing (progress/amount/ETA) on failed refunds, EarningsBalanceHeader's explicit 'owed back' state instead of a bare minus sign — these are design decisions, not accidents; any restyle must preserve the copy and the state logic. - DocumentUpload's complete state machine — idle → uploading (progress %) → success (✓ + preview) → error (retry) → rejected (reason + re-upload, never a dead end) — with client-side type/size validation, object-URL cleanup, and server-metadata as the only 'uploaded' truth. - CountdownTimer's isolation architecture: self-owned 1-second tick so only it re-renders, server-frozen deadline (never recomputed client-side), single onElapsed fire, tabular-nums digits. - Exhaustive typed enum→chip mappings (PaymentStatusBadge, EarningsRow, PayoutHistoryRow, InstallmentScheduleRow): a wire-enum change fails the build instead of rendering an unmapped status — keep this pattern in any badge redesign. - StatusChip as the single source of status color+icon that BankStatusPanel, VariantCard, PaymentStatusBadge, EarningsRow, PayoutHistoryRow, RefundStatusCard, CancellationPolicyDisclosure all delegate to — the consolidation point already exists; redesign the one component, not seven. - Accessibility groundwork: keyboard handlers + role=button on tappable cards (NurseResultCard, DocumentUpload dropzone), radiogroup/radio semantics (RatingInput, RelationSelect), aria-pressed on toggle chips, focus-visible outline on NurseResultCard, and data-* hooks on every component for tests. - Presentational purity with caller-owned i18n (labels are i18n keys off stable codes, never derived from wire values) — this is precisely what makes a ground-up visual redesign cheap and low-risk; don't let a restyle introduce data-fetching or hard-coded strings into this layer.