Files
baya-monorepo/archive/post-phase/ui/ui-phase-12-copy-motion-and-polish.md
T
2026-08-02 18:48:32 +03:30

23 KiB
Raw Blame History

UI Phase 12 — Copy, motion & final polish

Mission: the closing sweep of the UI chain — make the product read and move as one: a checked-in Persian style guide enforced across both catalogs, the verified copy defects on trust-critical strings fixed, a restrained motion language behind a single reduced-motion gate, an accessibility pass, and a final four-axes QA of everything the chain touched. Copy is the cheapest trust lever Balinyaar has — and today the brand name is spelled two ways, a nurse-facing EVV error reads as "there is no game-entrance", and one BNPL sentence states the inverse of the intended risk allocation.

Track: frontend · Depends on: all prior phases — 011 (run this last) · Unlocks: ships the chain — one voice, one motion language, verified on all axes. Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the frontend-designer skill — both are mandatory.

1. Context — where this sits

Balinyaar is a trust-first product in a market where written register carries weight: families decide whether to let a stranger into a parent's home partly on whether the product sounds careful. The catalogs are already hand-written — the weaknesses are craft, not tone. Verified in client/messages/fa.json / en.json (line numbers spot-checked 2026-07-16; earlier phases may shift them — grep the key, not the line):

  • The brand name is spelled two ways: «بالین یار» (plain space) in 5 keys — common.brand (53), auth.customer_title (625), auth.select_role_title (645), auth.account_error_body (654), verification.start_body (663) — vs «بالین‌یار» (ZWNJ) in 17: login and escrow copy disagree on the name.
  • تأیید — the most frequent word in a verification product — appears 38× without hamza and 26× with, no pattern (bank.status_verified_title vs admin.ver_pass); ~64 occurrences to sweep.
  • Two grammar bugs read as nonsense: booking.evv_no_open_check_in (542) «ورود بازی برای ثبت خروج وجود ندارد» and admin.alert_empty (1360) «هشدار بازی وجود ندارد» — the indefinite ی attached to the wrong word. And a typo sits on a trust chip: bank.status_verified_chip (180) reads «تاییدشد» (missing final ه) — the chip a nurse stares at while IBAN ownership verification runs.
  • One en arrow points the wrong way: booking.continue_payment (en 445) is "Continue to payment ←" while its sibling payment.cta_pay (577) is "…→". Five keys per catalog embed ←/→ literally (445, 577, 632, 633, 1337) — directional glyphs inside translatable strings are how this bug happens.
  • fa lacks ICU plurals where en has them: search.cta_view_results (fa 350) is «مشاهده {count} پرستار» — the primary search CTA can render «مشاهده ۰ پرستار»; en has ICU but its =0 case renders "View no nurses".
  • BNPL copy is banker's language with an ambiguous pronoun: bnpl.ownership_note (857) «…ریسک نکول مشتری کاملاً با اوست…» — «نکول» is credit-desk jargon and «با اوست» leaves the reader unsure whether the customer carries the default risk (the intended meaning: the provider does).
  • Policy numbers are hard-coded into legally-sensitive copy the admin config panel can change: the ۷۲-hour dispute window (payouts.explainer_point_2, 958), the ۲۴-hour cancellation tiers (refunds.lead_gt_24h/ lead_lt_24h, 789790), the ۷–۱۰-day refund ETA (refunds.eta_business_days, 850). One config edit makes the UI lie — and no public config read exists (platform_config/* in services/admin/ is admin-only).
  • Motion and live-region a11y are absent, app-wide: zero prefers-reduced-motion handling and zero aria-live anywhere in client/src; AppIconButton names icon-only actions solely via Tooltip title, dropped entirely when disabled (AppIconButton.tsx:8995).

What already exists (do not rebuild):

  • Phase 0's token extension (motion/focus/elevation tokens in theme/tokens.css) and single-weight icon registry with a mirrored chevron — this phase applies motion tokens, it does not invent them.
  • Phase 1's shared primitives: utils/number.ts formatNumber (replacing the 25+ inline fa-IR ternaries), the state-view kit, CountdownTimer v2, route-level loading.tsx/error.tsx.
  • Full key parity between fa.json and en.json (28 namespaces) with complete loading/empty/error/confirm/ toast coverage per namespace, and the trust copy that is already right (keep-list, §5): payment.escrow_notice, payouts.explainer_point_13, calming refund-status vocabulary, two-stage disclosure copy, the specific verification-failure reasons.
  • Phases 4/6/8/9 may already have fixed items this phase lists conditionally (3.2 address hint, 3.5 pipeline naming, 3.7 cities, 3.10 desktop layouts) — read their reports first and skip what is done.

2. Required reading (do this first)

  • audit/microcopy.md — the 16 copy problems + 7 opportunities this phase executes; its keep-list is binding. Then audit/cross-cutting-ux.md — motion/a11y/responsive findings (AppIconButton, motion absence, desktop phone-column) and its keep-list.
  • Prior phase reports in ../../shared-working-context/reports/ (ui-phase-0ui-phase-11) — to learn which conditional items are already done and what naming phase 8 chose for the verification journey.
  • client/messages/fa.json + en.json — read the namespaces you will sweep (booking, verification, payment, auth, common, bnpl, search, refunds, payouts, admin, shell, tickets, bank, address).
  • Code (under client/src/): components/common/AppIconButton/AppIconButton.tsx, components/CountdownTimer/, components/EscrowNotice/EscrowNotice.tsx, theme/tokens.css (phase-0 motion/focus tokens), utils/number.ts (phase-1 formatNumber), and for 3.10 app/[locale]/(private-routes)/(customer)/bookings/checkout/ + …/(customer)/search/results/page.tsx.
  • Product: ../../../product/overview/platform-summary.md §"Glossary" — consult it before deciding پرستار/مددجو/بیمار; the cancellation/refund docs under ../../../product/business/ before touching policy-number copy.
  • The design contract: ../../../.claude/skills/frontend-designer/SKILL.md (§7 non-negotiables) and client/CLAUDE.md "Golden rules". REQ tracker: ../../shared-working-context/frontend/requests/for-backend.md — find the tail (REQ-038 at authoring time; earlier UI phases may have appended) and number onward.

3. Scope — build this

3.1 The Persian style guide — client/messages/STYLE.md + enforcement

Write a one-page style guide the catalogs are then swept against, fixing these decisions:

  • Brand = «بالین‌یار» (ZWNJ), always. Sweep the 5 plain-space keys.
  • Hamza form «تأیید» everywhere (~64 occurrences to normalize — the 38 hamza-less ones change).
  • One form of جستجو. Standardize on «جستجو» (the 9-key majority; fold in the 2 «جست‌وجو» keys).
  • ZWNJ rules: می‌ + verb, plural ها, compound adjectives («تأییدشده»); codify with examples.
  • Punctuation & quotes: Persian «،» / «؛» / guillemets; en uses straight apostrophes (the admin namespace's curly 'dont' normalizes to the file's straight-quote majority).
  • Domain glossary: پرستار / بیمار vs مددجو — decide against the product glossary; «مددجو» appears exactly once (booking.patient_label), so adopt it consistently or drop the one-off. One status vocabulary shared by nurse-facing and admin-facing keys (see 3.5).
  • Shell naming system: one metaphor per audience class instead of today's four (shell.*, fa 56–61: اپلیکیشن/نما/کنسول/پرتال). Recommended: end-user shells = «اپلیکیشن» (خانواده، پرستار), back-office shells = «کنسول» (مدیریت، همکار).
  • Register: formal شما with polite imperatives (کنید) — already consistent; codify so it cannot drift.
  • Digits policy: Persian digits in fa for both literals and interpolations (mechanics in 3.3).

Enforcement: add a ~30-line Node script (client/scripts/check-copy.mjs) that greps fa.json for the banned variants (space-brand, hamza-less تایید, the «بازی» word-boundary trap, جست‌وجو, می‌گردد) and exits non-zero; wire it as npm run lint:copy and into npm run check. 60+ scattered findings become a one-time fix that cannot regress.

3.2 Verified copy-bug fixes (each independently shippable)

  1. booking.evv_no_open_check_in → «ورودِ ثبت‌شده‌ای برای این ویزیت وجود ندارد؛ ابتدا ورود را ثبت کنید.» and admin.alert_empty → «هشداری برای رسیدگی نیست.»
  2. address.line_hint «…برای یافتن در نیاز دارد» → «هر جزئیاتی که پرستار برای پیدا کردن منزل شما لازم دارد.» — only if phase 9 didn't. And bank.status_verified_chip «تاییدشد» → «تأییدشده».
  3. Archaic passive «می‌گردد» → «می‌شود»: refunds.confirm_restate (814), admin.mod_confirm_publish (1314), admin.cfg_save_confirm_body (1333) — three occurrences, not one as the audit counted.
  4. tickets.thread_empty_body comma splice → «هنوز پیامی نیست. هماهنگی را شروع کنید.»
  5. en normalization: auth.nurse_subtitle "licence" → "license" (American throughout — the file already uses "center" 25×); straighten the admin namespace's curly apostrophes.
  6. Introduce EVV in Persian once, then abbreviate: a first-occurrence key «ثبت حضور الکترونیکی (EVV) — ورود و خروج شما ثبت می‌شود تا ویزیت بدون اختلاف تأیید شود» on the nurse's visits surface (booking.evv_*, fa 528535), short chips («ثبت ورود») thereafter — EVV is why the nurse gets paid without arguments; sell it.

3.3 ICU plurals + designed zero cases

Every {count} key gets an ICU plural form in both catalogs with a designed =0 case: fa search.cta_view_results (350), results_count (352), reviews_count (364), booking.session_count (515) plus a grep sweep for the rest. Zero states are copy, not numerals — «پرستاری یافت نشد» / "No nurses found"; also fix en's awkward =0 label ("View no nurses"). Digits policy: fa interpolations render Persian digits — use {count, number} / ICU # (locale-formatted by next-intl) in message keys, and phase 1's formatNumber where a raw value is interpolated at a call site; verify one of each renders «۳» not "3".

3.4 Arrows out of strings

Remove the five literal ←/→ glyphs per catalog (booking.continue_payment, payment.cta_pay, auth.nurse_switch, auth.customer_switch, admin.cfg_history_change — fa/en lines 445/577/632/633/1337) and fix en 445's wrong-direction arrow by construction: direction moves into auto-mirroring endIcon slots on the buttons involved (phase 0's chevron via the AppIcon registry; the RTL Emotion cache mirrors it). admin.cfg_history_change «{old} ← {new}» becomes two interpolations joined by a component-rendered mirrored arrow icon. Touch each call site; the strings become text-only.

3.5 Trust-moments copy pass

  • OTP screen reassurance: auth.customer_subtitle (626) is just «با شماره موبایل خود وارد شوید» — add one warm trust line (verified nurses + escrow, e.g. «پرستاران تأییدشده، پرداخت امن نزد بالین‌یار»).
  • Checkout «چرا امن است»: one line near the pay CTA linking escrow to the dispute process — supplementing, never replacing, payment.escrow_notice (§5). And post-payment what-happens-next: the confirmation states the next steps in order (nurse notified → coordination ticket → visit-day EVV).
  • BNPL de-jargon: rewrite bnpl.ownership_note (857) reader-first, plainly allocating risk to the provider — e.g. «قسط‌ها را مستقیماً به {provider} می‌پردازید؛ بالین‌یار مبلغ کامل را همان ابتدا دریافت می‌کند و اگر قسطی پرداخت نشود، مسئولیت آن با ارائه‌دهنده است، نه شما و نه پرستار.» «نکول» disappears from the catalog.
  • Disambiguate the verification names: «احراز هویت» names both the 7-step pipeline (nav.verification 13, verification.title 658) and the KYC step inside it (step_identity_kyc 676, admin 1470). Rename the pipeline «تأیید صلاحیت»; the KYC step keeps «احراز هویت». Coordinate with phase 8's journey naming — if its report already renamed, adopt its term and only sweep stragglers.
  • One rejected-status vocabulary: unify رد شد / ناموفق / ردشده into one nurse-facing and one admin-facing form (decide in STYLE.md; sweep verification.* and admin.* together — they cross-reference).

3.6 Config-served policy numbers (REQ + client single-sourcing)

Parameterize the hard-coded policy numbers: payouts.explainer_point_2 takes {hours} (72), refunds.lead_gt_24h/lead_lt_24h take {hours} (24), refunds.eta_business_days takes {minDays}/ {maxDays} (710). File a REQ (next free number) for a public/authenticated policy-config read (dispute-window hours, cancellation tiers + percentages, refund ETA) — verify first that no existing endpoint serves it (at authoring time only admin-scoped platform_config/* exists). Until served, single-source the constants in client/src/constants/policy.ts with a comment linking the REQ, and feed the interpolations from there. One config edit must never again silently make copy lie.

3.7 Honest search empty state

Replace search.empty_suggest_city (361, both catalogs) — «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را امتحان کنید» is nonsense advice for a Tehran-launch user — with honest relaxations already next to it (clear the district, widen filters) or interpolated actually-covered cities. Only if phase 4 didn't.

3.8 Motion pass — one restrained language

Apply phase 0's motion tokens app-wide; introduce nothing springy — this is a calm clinical product:

  • Page/content transitions: 150200ms fade + small slide on route-group content; skeleton → content crossfade (the phase-1 skeleton twins).
  • Dialog/bottom-sheet enter/exit via theme.components defaults (one place, not per-dialog); list-entrance restraint — at most a subtle stagger on first paint, never re-animating on refetch.
  • prefers-reduced-motion is respected via a single media-query gate in one place (a token/theme-level switch that zeroes durations) — today the app has zero handling; do not scatter per-component checks.

3.9 A11y sweep

  • AppIconButton: add an aria-label passthrough that defaults from title and survives disabled (today the Tooltip — the only name source — is dropped when disabled; AppIconButton.tsx:8995). Update its test.
  • aria-live on the moments that change without focus: CountdownTimer (polite), async error/retry regions, payment-status polling. Today aria-live appears nowhere in client/src.
  • Disclosure semantics (aria-expanded + aria-controls) on collapsibles — the earnings ExplainerCard pattern and any phase-built equivalents.
  • Form label association audit (MUI does this when label is used — audit the hand-rolled ones); focus-visible coverage check (phase 0 tokenized the ring — verify interactive elements show it); contrast re-check on remaining terracotta text usages in both schemes.

3.10 Desktop-aware layouts — the two worst phone-column offenders (scope-boxed)

Only if phases 4/6 didn't already: above ~900px, (a) checkout ((customer)/bookings/checkout/) becomes two-column with a sticky order summary; (b) search results ((customer)/search/results/page.tsx) uses the width (wider cards or list + detail). Nothing else — a full responsive pass is (DEFERRED → post-chain).

3.11 Final QA — the closing walkthrough

Write a per-shell walkthrough checklist (customer funnel, nurse day-of + verification, admin desks, partner portal, auth/first-run) × /fa+/en × light+dark × mobile+desktop. Run it end-to-end; fix what it finds (anything non-trivial becomes a report note, not silent scope expansion). Attach the completed checklist to the phase report — it is the chain's shipping evidence.

4. Mocks & seams in this phase

None introduced. UI stays mock-tolerant behind the existing services/{domain} seams. The one backend gap is 3.6's policy-config read: a REQ appended to for-backend.md (REQ-001…038 taken at authoring time — check the tail for numbers earlier UI phases claimed, and number onward). Until delivered, the client single-sources the numbers in constants/policy.ts; no mock flag changes.

5. Critical rules you must not get wrong

  • payment.escrow_notice and product-mandated strings change only with a flagged product note. The escrow sentence, payouts.explainer_point_13 (especially point 3's BNPL-fee-never-deducted guarantee), the two-stage disclosure copy (booking.notes_hint / booking.disclosure_note), and the verification-failure reasons are model trust writing — sweeps may normalize their spelling, never their meaning. If a rewrite seems needed, flag it in the report instead.
  • Don't regress the keep-lists (both audit files): calming money-status vocabulary («در راه»، «نیازمند بررسی»), culturally-tuned gender copy, formal شما, complete per-namespace state strings, admin confirm-dialog operational candor, token discipline, RTL dir="ltr" islands, Shamsi-first formatting.
  • en stays hand-written-idiomatic — no machine-translation tone ("Queue clear — nothing to review" is the bar). Key parity fa/en stays absolute: every add/rename lands in both catalogs in the same change.
  • Don't rename stable message keys casually. A key rename must grep and update every t('…') usage; if a key's meaning is unchanged, keep its name and change only the value.
  • Design-contract non-negotiables that bite here: tokens not hexes (motion durations/easings are tokens, not inline magic numbers); RTL logical props (arrows mirror by construction); dark mode on every touched surface; MUI v9 API only; co-located tests for touched shared components (AppIconButton, CountdownTimer, EscrowNotice all have .test.tsx); fetch/cookies rules untouched — no data-layer changes.
  • Reduced motion is a hard gate, not a nice-to-have — every animation collapses to no-motion under prefers-reduced-motion: reduce via the single gate (3.8). Frontend lane only: the policy-config gap is a REQ, never a server/ edit.

6. Definition of Done

On top of the shared definition-of-done.md:

  • npm run check green (now including lint:copy); npm run test:ci green for touched shared components (AppIconButton, CountdownTimer, any collapsible/state component touched).
  • client/messages/STYLE.md exists; scripts/check-copy.mjs passes with zero banned variants — no plain-space brand, no hamza-less تایید, no «بازی» trap, no می‌گردد, one جستجو form.
  • en.json/fa.json in key parity; every {count} key has ICU plural + designed =0 in both catalogs; no literal ←/→ remains in either catalog (grep proves it).
  • bnpl.ownership_note plainly allocates default risk to the provider; «نکول» absent from the catalog.
  • Policy numbers interpolate from constants/policy.ts; the policy-config REQ is filed with a number.
  • prefers-reduced-motion: reduce disables all chain-added motion; icon-only buttons have accessible names including when disabled; countdown/async-status regions have aria-live; touched collapsibles expose aria-expanded.
  • Visual verification on the four axes — /fa + /en × light + dark — and mobile + desktop for 3.8/3.10 surfaces; the completed 3.11 walkthrough checklist is attached to the phase report.

7. How to test (what a human can verify after this phase)

  1. cd client && npm run check — passes, including the new copy lint. Temporarily add «بالین یار» to any fa value → lint:copy fails; revert.
  2. Log in as a customer (/fa): login, checkout escrow copy, and refund screens all spell «بالین‌یار» identically; the OTP screen shows the new reassurance line.
  3. Search with filters that match nothing → «پرستاری یافت نشد» (not «۰ پرستار»; not "View no nurses" on /en); counts render Persian digits on /fa.
  4. As a nurse, attempt EVV check-out without a check-in → the corrected sentence (no «ورود بازی»); the bank screen's verified chip reads «تأییدشده»; the visits screen introduces «ثبت حضور الکترونیکی (EVV)» once.
  5. On /en checkout, "Continue to payment" carries a mirrored end-icon pointing forward (→ in LTR, mirrored on /fa); grep both catalogs for ←|→ → zero hits.
  6. Open the BNPL comparison → the ownership note reads plainly, no «نکول», risk clearly on the provider. Grep the catalogs for «۷۲ ساعته» → none; the keys take {hours} from constants/policy.ts; the REQ exists in the tracker.
  7. Navigate between routes → 150200ms calm fade/slide, skeletons crossfade into content; enable reduced motion in DevTools → everything appears instantly.
  8. Inspect the a11y tree on a screen with icon-only buttons: each has a name, including a disabled one; a running countdown announces politely (aria-live).
  9. Run the 3.11 checklist across all shells on the four axes + both widths; every row checked or fixed.

8. Hand off & document (close the phase)

  • Update client/CLAUDE.md: mention messages/STYLE.md + lint:copy in the i18n/Golden-rules sections and the reduced-motion gate location; update "Project Structure" if scripts/ or constants/policy.ts are new.
  • Write the frontend report at ../../shared-working-context/reports/ui-phase-12-report.md: the style-guide decisions taken (hamza form, جستجو form, shell naming, status vocabulary, digits policy), swept-key counts, conditional items skipped because an earlier phase did them, the completed QA checklist, and anything the walkthrough found but deferred. List the REQ(s) filed with their numbers.
  • Save a memory note per operating-rules §8: the chain is complete — style guide + copy lint now enforce Persian orthography; motion and a11y gates exist; record the STYLE.md decisions so future copy follows them.