cleanup phases 6

This commit is contained in:
hamid
2026-08-02 18:48:32 +03:30
parent e2db97392a
commit 51e86a1e5f
239 changed files with 118 additions and 70 deletions
@@ -0,0 +1,296 @@
# UI Phase 1 — Shared primitives & app-wide states
> **Mission:** build the shared component kit the pages keep hand-rolling — state views, page header, confirm
> dialog, card anatomy, `<Money>`, locale formatting, a Jalali date picker, status/timeline/countdown/rating
> upgrades, skeleton twins — give the app real route-level chrome (loading / error / 404 / per-route metadata),
> and fix the error→false-empty defects the missing kit caused. This phase **owns every shared primitive**
> (see the [README ownership rules](README.md)); phases 311 only consume what ships here.
>
> **Track:** frontend · **Depends on:** [Phase 0](ui-phase-0-design-language.md) (theme pass, icon family,
> `--bal-money-emphasis` / `--bal-rating*` / elevation / motion / focus tokens) · **Unlocks:** the kit every
> area redesign (phases 311) composes from
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the `frontend-designer` skill — both are mandatory.**
## 1. Context — where this sits
Balinyaar's feature layer is disciplined (tokens, RTL, four-state data pattern), but the *page primitives*
were only ever built for the backoffice. Everything user-facing is hand-rolled per page, so the app reads as
a starter and every restyle is a 20-file change. Verified in code:
- **29 hand-rolled dashed-border `Paper` empty/error blocks across 23 files** (grep `border: '1px dashed'`),
while `AdminEmptyState`/`AdminErrorState` sit unused outside `components/admin/`.
- **24 hand-assembled `formatIrrToToman(x) + t('currency_toman')` sites across 19 files**; `PriceBreakdown.tsx:62`
colors the grand total terracotta (`--bal-secondary`, ≈2.7:1 on white — a WCAG failure on the most
trust-critical number), a pattern repeated across five sibling money components.
- **23 copies of the `locale === 'fa' ? 'fa-IR' : 'en-US'` ternary across 15 files**, already drifting
(`admin/partners/[id]/page.tsx` passes the raw locale); **no relative-time formatting anywhere**; the OTP
resend clock renders Latin digits (`components/auth/OtpStep.tsx:27`).
- **Zero `loading.tsx` / `error.tsx` / `not-found.tsx` / `global-error.tsx`** under `client/src/app`; the only
crash UI is the starter `ErrorBoundary` (English `<h2>`, raw `componentStack` dump); one static `<title>`
serves ~60 routes; **no Jalali date input** — every date input is a native Gregorian `type="date"`.
- **The missing kit caused real defects.** A failed query renders as *success-with-no-data*:
- `(customer)/page.tsx:5768` — the page gate destructures only `data` from `usePatients()`; on error it
returns `<AppLoading />` forever (spinner-hang). The `CategoryGrid` in the same file branches all four
states correctly — the discipline exists, the gate doesn't.
- `(customer)/patients/page.tsx:83` (`isEmpty = !isLoading && …length === 0` → error shows «no patients»);
`nurse/requests/page.tsx:19` (error = empty inbox); `nurse/services/MyServicesList.tsx:37,42` (error =
«add your first service» CTA).
- `(customer)/profile/page.tsx:16` — an errored profile renders a **blank form** whose save could overwrite
server truth; `nurse/services/VariantBuilder.tsx:384``optionGroupsQuery` has no error branch, so failed
option groups are treated as "category has none", letting a nurse skip required options.
- **Silent mutations:** `nurse/profile/page.tsx:44` (avatar upload) and `:5464` (profile save) pass only
`onSuccess` — a failure gives zero feedback.
**What already exists (do not rebuild):**
- The admin primitives to *promote*, not rewrite (all in `client/src/components/admin/`, all tested):
`AdminPageHeader` (title/subtitle/actions, caller-owned i18n), `AdminEmptyState`/`AdminErrorState`,
`ConfirmDialog` (required-reason gating + busy-disable).
- `utils/money.ts` (BigInt IRR↔Toman, fa digits) and `utils/date.ts` (Shamsi via `Intl` `fa-IR-u-ca-persian`,
no date library); `CountdownTimer`'s architecture — server-frozen UTC deadline prop, self-owned 1s tick
(only it re-renders), single `onElapsed` fire, `dir="ltr"` tabular-nums locale digits (`CountdownTimer.tsx:84`).
- `StatusChip` as the single status color+icon source seven components delegate to; the exhaustive typed
enum→chip mappings; `TrustBadge`'s honest three states; the four-state pattern itself (~95 `isError`
branches, ~124 retries) — restyle and fill gaps, don't rebuild; the `dispatchToast → notistack` pipeline.
- Phase 0's output: `theme.components` pass, one icon family, the new tokens. Build on them.
## 2. Required reading (do this first)
- [audit/component-primitives.md](audit/component-primitives.md) (kit inventory, hand-roll counts),
[audit/feature-components.md](audit/feature-components.md) (card-anatomy drift, rating/terracotta/timeline
defects), [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) (route chrome / metadata / formatting) —
each carries the full evidence and a keep-list.
- [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — the design
contract; and `client/CLAUDE.md` — Golden rules, Project Structure, Unit Testing, Toast Notifications, i18n.
- Code to read before writing: `client/src/components/admin/{AdminPageHeader,AdminEmptyState,AdminErrorState,ConfirmDialog}.tsx`;
`client/src/components/{StatusChip,CountdownTimer,RatingInput,StepperHeader,RefundStatusCard,PriceBreakdown}/`;
`client/src/components/booking/BookingStatusTimeline/`; `client/src/utils/{money,date}.ts`;
`client/src/lib/api/client.ts` (which errors the fetch layer already toasts — 3.14 builds on it);
`client/src/theme/tokens.css` as phase 0 left it. Product framing: `product/overview/platform-summary.md`
(money display and Shamsi correctness are product rules, not preferences).
## 3. Scope — build this
New shared components live under `client/src/components/common/<Name>/` (component + `index.tsx` barrel +
co-located `.test.tsx`), exported from `@/components`, presentational with **caller-owned i18n** (already-
translated strings in, like `AdminPageHeader` documents). Strings a component *does* own (route chrome,
default retry labels) go in **both** `messages/en.json` and `messages/fa.json`.
### 3.1 State kit — `EmptyState`, `ErrorState`, `QueryStateGate` + the false-empty fixes
- Promote/generalize `AdminEmptyState`/`AdminErrorState` into shared `EmptyState` and `ErrorState` with a
branded look (phase 0 surface/radius tokens; registry icon slot; calm — retire the dashed border).
`EmptyState`: icon, title, body, optional CTA. `ErrorState`: **`onRetry` is required** («تلاش دوباره»).
`components/admin` keeps thin wrappers or its call sites migrate — either way no duplicated implementation
remains. (Custom illustration set: DEFERRED → phase 12.)
- Add a small `QueryStateGate` (props: `isLoading`, `isError`, `onRetry`, `isEmpty`, `skeleton`, `empty`,
`children`) rendering skeleton → error → empty → data in that order, **plus** write the convention into
`client/CLAUDE.md`: *an errored query must never render as empty; every error branch has a retry.* Pages
with naturally inline branching (infinite lists) may follow the convention without the component.
- Fix the six defects from §1 with it. The profile fix must render `ErrorState` instead of the blank form;
VariantBuilder's option-group error must block progression, not pretend "no options".
- Mechanical sweep: replace the 29 dashed-`Paper` blocks in 23 files with `EmptyState`/`ErrorState`. Copy
stays per-page (existing keys); only the shell unifies.
### 3.2 `PageHeader` + `ConfirmDialog` promotions
- Generalize `AdminPageHeader` into a shared `PageHeader`: `title`, `subtitle?`, `actions?`, plus a `backTo?`
affordance (RTL-flippable chevron via the phase 0 icon family; if phase 0 shipped no `back` icon, extend
`AppIcon/config.ts` minimally and note it — ownership rule). Adopt it **mechanically** where pages already
hand-roll the identical h5/h1 + subtitle block (patients, addresses, bookings, earnings, …); per-page header
design is each area phase's job (DEFERRED → 311). `AdminPageHeader` becomes a re-export or its call sites migrate.
- Move `components/admin/ConfirmDialog` to `components/common/ConfirmDialog` **preserving its contract**:
required-reason gating, busy state disabling both buttons, caller-owns-the-mutation. Migrate the admin
imports and the hand-rolled confirm flows only where mechanical (patients archive, addresses delete); the
remaining raw `<Dialog>` confirm sites are area-phase work (DEFERRED → 311).
### 3.3 Card kit — `SurfaceCard` + `AccentCard`
Encode the de-facto anatomy once: `SurfaceCard` = flat `Paper` (`elevation={0}`), 1px `divider` border, house
radius, a padding scale (`sm`/`md`/`lg` ≈ today's p:2 / 2.5 / 3); `AccentCard` = `SurfaceCard` + a
`borderInlineStart` accent taking a **semantic token name**, at one standardized width (pick one; end the
3px/4px drift). Migrate the ~12 hand-rollers mechanically — `EarningsRow`, `EarningsBalanceHeader`,
`PayoutHistoryRow`, `PatientCard`, `VariantCard`, `VisitNoteCard`, `InstallmentScheduleRow`, `PriceBreakdown`,
`BankStatusPanel`, `DocumentUpload`'s panels, `BookingRequestSummaryCard`, the customer-home `NudgeCard`
visual unification only, zero content/behavior change.
### 3.4 `<Money>` primitive
One component for every displayed amount: takes a **served IRR digit-string**, renders Toman + unit label via
`utils/money.ts` (never computes); `size`/`tone` variants with emphasis driven by `--bal-money-emphasis`
(phase 0) — **not terracotta**; deduction rendering (muted/error tone with an explicit visual sign treatment,
never a bare minus inside RTL text); optional strikethrough. Replace the 24 hand-assembled sites in 19 files —
this fixes the `PriceBreakdown` total-contrast defect and the sibling terracotta-money sites (`RefundStatusCard`,
`BnplPlanCard`, `InstallmentScheduleRow`, `CancellationPolicyDisclosure`) in one pass. `PriceDisplay` (catalog
unit rates) stays; refactor it onto `<Money>` internals only if trivial — its contract is tested and correct.
### 3.5 Formatting utils — `utils/number.ts`
- `localeTag(locale)``'fa-IR' | 'en-US'` and `formatNumber(value, locale, options?)` — kill the 23
copy-pasted ternaries in 15 files (including the drifted `admin/partners/[id]/page.tsx` raw-locale call).
- `formatRelativeTime(iso, locale)` via `Intl.RelativeTimeFormat`; **rule (document in JSDoc): relative up to
~7 days («۲ ساعت پیش»، «دیروز»), then decay to the absolute Shamsi date** via `formatShamsiDate`. Consumers
land in phases 10/11; ship the util + tests now.
- `formatClock(totalSeconds, locale)` — locale-digit `mm:ss`/`hh:mm:ss`; migrate `CountdownTimer`'s inline
`Intl` pad onto it and fix `OtpStep.tsx:27`'s Latin-digit resend clock.
### 3.6 `JalaliDatePicker` + `JalaliDateField`
Shamsi-native date selection that **emits ISO Gregorian** (the wire stays Gregorian; display stays Shamsi):
a month-grid calendar (Persian month/weekday names, today marked, min/max props), a compact **day-chip-strip**
variant for near dates (next N days as tappable chips — the C4 booking form's shape), and `JalaliDateField`
(read-only input + picker popover) to replace native `type="date"` inputs. **Investigate and DECIDE the
arithmetic layer:** prefer `Intl` `fa-IR-u-ca-persian` (already proven in `utils/date.ts`) for display + a
minimal conversion for month math; a tiny, well-maintained jalali lib (e.g. `jalaali-js`, ~2 KB) is acceptable
if `Intl`-only arithmetic gets awkward — document the choice and measured bundle cost in the report; no
heavyweight date library. Locale-aware (`fa` Persian calendar, `en` may show Gregorian — same ISO output),
keyboard-navigable, RTL-correct, both schemes. Consumed by phases 4, 5, 8, 11 — consumer migration is theirs
(DEFERRED → 4/5/8/11).
### 3.7 `StatusChip` v2
Soft-tint default: `-soft` token backgrounds + strong text (extend `tokens.css` + `colors.ts` with any missing
`-soft` pairs, both schemes); solid fill reserved for high-alarm states; a documented status weight hierarchy
(neutral < info < progress < success < warning < alarm). `StatusChip` is the delegation point, so this is
mostly one file; update direct chip consumers mechanically and keep the exhaustive enum→chip mappings intact.
### 3.8 `StatusTimeline`
A designed vertical timeline: node = label + optional timestamp + optional note; states completed / current
(animated within the phase 0 motion tokens, reduced-motion-safe) / pending / terminal-failure. Migrate the
two StepperHeader-as-status-display misuses: `RefundStatusCard.tsx:76` (3-step refund progress) and
`components/booking/BookingStatusTimeline` (7-status booking lifecycle). **StepperHeader remains for real
wizards only** (onboarding, verification, BNPL) — write that into its JSDoc.
### 3.9 `CountdownTimer` v2
Keep the do-not-regress architecture (server-frozen deadline, isolated tick, single `onElapsed`, `dir="ltr"`
tabular digits). Add: a **progress-ring variant** (given `windowStart`); **urgency tiers** — calm teal →
amber → terracotta — with threshold props, replacing the binary `urgent`; **humanized coarse mode** above
~10 minutes («۲۵ دقیقه مانده», minute-grain updates) switching to the ticking clock below it; keep the `label`
slot; `aria-live="polite"` at coarse intervals (never per second). Swap the hourglass `pending` glyph for the
clock; drop the hard-coded `1.5rem` for size variants. Existing consumers (C5 tracker, nurse inbox, checkout,
EarningsRow dispute window) keep working unchanged — new props optional.
### 3.10 `RatingInput` v2
Switch fill colors to phase 0's `--bal-rating` / `--bal-rating-empty`; **fractional fill** (dual-layer or clip)
for read-only averages so 4.5 renders as 4.5 — then remove the `Math.round` workaround at
`search/nurse/[nurseId]/page.tsx:260` (it shows a 4.5 average as 5 stars — an honesty bug on the core trust
surface); size variants; keep the radiogroup/readOnly `role="img"` semantics and `data-*` hooks. Swap the star
color in `NurseResultCard` and `BookingRequestSummaryCard` to the same tokens.
### 3.11 Skeleton twins + `AppLoading` rebrand
Co-located `<X.Skeleton />` statics matching real anatomy (avatar disc, chip row, price line) for
`NurseResultCard`, `EarningsRow`, `PayoutHistoryRow`, `PatientCard`, `VisitNoteCard`, `TicketListCard`; adopt
at the loading branches that hand-guess heights (e.g. `search/results/page.tsx:75`). Rebrand `AppLoading` into
a branded splash (brand mark + subtle motion, reduced-motion-safe) used **only** for true full-page waits
(auth splash, gateway return). Of the 18 screens using it today, switch the ones with a shaped skeleton
available to skeletons; the rest keep the splash until their area phase.
### 3.12 Route chrome — loading / error / 404
Per-route-group `loading.tsx` with shell-shaped skeletons (`(customer)` header + card stack; `nurse`/`admin`/
`partner` sidebar-shell content; `(public-routes)` auth card). Branded `error.tsx` (per `[locale]` segment)
and `not-found.tsx` + the next-intl catch-all 404 pattern — calm Persian-first copy, brand mark, retry
(`reset()`) and «بازگشت به خانه» CTAs, keys in both catalogs. `global-error.tsx` (sits above `[locale]`,
renders its own `<html>` — it cannot use next-intl): minimal static **bilingual** fa+en copy — the one
sanctioned hard-coded-string exception; note it in `client/CLAUDE.md`. Rewrite `components/common/ErrorBoundary`
to the same design: i18n'd, branded, retry affordance, **stack/componentStack rendered only in development**
(logged, never shown, in prod).
### 3.13 Per-route metadata
Title template via one `generateMetadata` in `app/[locale]/layout.tsx`: `'%s | بالین‌یار'` (fa) /
`'%s | Balinyaar'` (en) + localized description. Establish and document the client-page pattern — `page.tsx`
becomes a thin RSC exporting `generateMetadata` and rendering the co-located `'use client'` body — and apply
it to the main landing pages now (customer home, `/login`, `/search`, `/bookings`, `/nurse`, `/admin`,
`/partner`). Full per-page adoption continues in the area phases (DEFERRED → 311).
### 3.14 Mutation-error convention
Read `lib/api/client.ts` to confirm exactly which failures the fetch layer already toasts; then: **every
mutation whose failure is not surfaced inline or by the fetch layer gets an `onError` toast.** Fix the two
known silent ones — nurse profile save (`nurse/profile/page.tsx:5464`) and avatar upload (`:44`) — and write
the convention into `client/CLAUDE.md` (Toast Notifications section).
## 4. Mocks & seams in this phase
None — pure UI over existing `services/{domain}` seams; no new service, mock, or flag. Expected REQ count:
zero. If a genuine backend gap surfaces anyway, append it to
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
numbered **REQ-039 onward** (001038 are taken), keep the UI mock-tolerant behind the seams, never touch `server/`.
## 5. Critical rules you must not get wrong
1. **Presentational purity + caller-owned i18n stays.** Shared primitives take already-translated strings and
stable codes; no data-fetching, no wire-derived labels inside this layer.
2. **Money invariants stay.** Amounts are served IRR digit-strings through `utils/money.ts` (BigInt); `<Money>`
formats, never computes; `PriceBreakdown`'s dev reconciliation guard survives the card-kit migration.
3. **CountdownTimer architecture stays** — server-frozen deadline, isolated self-tick, single `onElapsed`;
deadlines never recomputed client-side. **ConfirmDialog's contract stays** through the move. **An error is
never an empty** — the defect class this phase kills; every error state keeps a retry.
4. **Don't break what delegates.** `StatusChip` is the single status source and the enum→chip mappings are
exhaustively typed — keep both; `StepperHeader` keeps serving the real wizards untouched; honest-copy
components (EscrowNotice verbatim fa, RefundStatusCard's no-success-framing-on-failure,
EarningsBalanceHeader's "owed back") keep their copy and state logic — restyle shells only.
5. **Design contract non-negotiables:** tokens not hexes (extend `tokens.css` + `colors.ts` together, both
schemes); logical/RTL-safe props only (keep the deliberate `dir="ltr"` islands for clocks/IBANs/phones);
both message catalogs in sync; MUI v9 API only; co-located tests for every shared component;
fetch/cookies/toast rules per `client/CLAUDE.md` untouched.
6. **Never add a layout above `[locale]`**`global-error.tsx` is a special file, not a layout, and is the
only file allowed static copy.
## 6. Definition of Done
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
- [ ] `npm run check` green; `npm run test:ci` green (many shared components are touched — run it); every new
shared component has a co-located `.test.tsx`; `en.json`/`fa.json` in sync.
- [ ] All six error→false-empty/spinner defects from §1 render `ErrorState` with a working retry (verify by
forcing the query to fail); the profile error never shows an editable blank form.
- [ ] Sweep greps clean: `border: '1px dashed'` under `client/src/app` → zero; `currency_toman` outside
`utils/money.ts` + `<Money>`/`PriceDisplay` internals → zero; `locale === 'fa' ? 'fa-IR'` → only
`utils/number.ts`. The checkout total renders in `--bal-money-emphasis`, not terracotta.
- [ ] `loading.tsx`, `error.tsx`, `not-found.tsx`, `global-error.tsx` exist and render branded, localized
(404/error verified on `/fa` and `/en`); ErrorBoundary shows no stack in a production build; browser
tab titles differ per section using the `%s | بالین‌یار` template.
- [ ] `JalaliDatePicker`/`JalaliDateField` render Shamsi, emit ISO Gregorian, pass keyboard/RTL tests; the
arithmetic decision + bundle cost is documented in the report.
- [ ] A 4.5 rating average renders a half-filled fifth star on the nurse profile (no `Math.round`);
`RefundStatusCard` and the booking timeline render `StatusTimeline` (wizards still use `StepperHeader`).
- [ ] Nurse profile save and avatar upload toast on failure; the convention is in `client/CLAUDE.md`.
- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for the state
kit, card kit, route chrome, and the Jalali picker.
## 7. How to test (what a human can verify after this phase)
1. Stop the backend (or force queries to reject via devtools offline mode) and load `/fa`: customer home shows
a branded error state with «تلاش دوباره» — not an infinite spinner. Restore, retry → home renders. Repeat on
`/patients`, `/profile`, `/nurse/requests`, `/nurse/services`: an error state, never «empty» copy or a blank form.
2. Visit a garbage URL (`/fa/xyz`) → branded Persian 404 with a home CTA. Throw inside a page in dev → branded
error screen with retry; build production and confirm no stack is rendered.
3. Navigate between sections and watch the tab title change («جستجو | بالین‌یار», «رزروها | بالین‌یار», …).
4. Open search results while loading: skeletons match the card anatomy — no layout jump when data lands. Open
checkout (mock path): the total renders in the money-emphasis color with correct contrast in both schemes;
a deduction row (earnings commission) reads visually as a deduction.
5. Open the C5 request tracker: progress ring, coarse «X دقیقه مانده» above 10 minutes, ticking Persian-digit
clock below, escalating tint as thresholds pass; the `/login` OTP resend clock shows Persian digits on `/fa`.
6. Open a nurse profile with a fractional average: stars render the fraction. Open a refund status page:
vertical timeline with node states, not a form stepper.
7. Mount `JalaliDateField` (the phase's test page or the first consumer): pick «۱۵ مرداد» → the emitted value
is the correct ISO Gregorian date; keyboard navigation works; `/en` shows the Gregorian view.
8. Kill the network mid-save on the nurse profile → an error toast appears; the form keeps the draft.
## 8. Hand off & document (close the phase)
- Update `client/CLAUDE.md`: Project Structure (new `components/common/*` primitives, route-chrome files,
`utils/number.ts`), the QueryStateGate/error-never-empty convention, the mutation-error toast convention,
the metadata pattern, and the `global-error.tsx` exception.
- Write the report at `dev/shared-working-context/reports/ui-phase-1-report.md`: what shipped, the Jalali
decision + bundle cost, icon-registry additions (if any), the sweep counts, any REQ entries appended to
[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-039+; expected none),
and any foundation gaps left for phase 2+.
- Save a memory note per operating-rules §8: the primitives kit now exists and later phases must consume it
(never fork local variants), the error-never-empty and mutation-toast conventions, and the Jalali decision.