manual improvement 1
This commit is contained in:
+50
-31
@@ -19,6 +19,8 @@ i18n, cookies, and the rules every change must follow.
|
||||
- **React 19** + **TypeScript** (`strict`).
|
||||
- **MUI v9** (`@mui/material`) for components and theming; **Emotion** underneath (RTL via
|
||||
`stylis-plugin-rtl`).
|
||||
- **Lucide** (`lucide-react`) for icons, behind the `AppIcon` name registry.
|
||||
`@mui/icons-material` was removed — never reintroduce it.
|
||||
- **next-intl v4** for i18n — locales `fa` (default, RTL) and `en`.
|
||||
- **TanStack Query v5** for server state; a small **AuthContext** (React context + reducer,
|
||||
`src/context/auth/`, seeded with server-read auth state) for auth/session state.
|
||||
@@ -121,8 +123,8 @@ client/
|
||||
│ ├── [...rest]/page.tsx # Catch-all — calls notFound() so any unmatched path under a locale renders not-found.tsx (next-intl's recommended 404 pattern)
|
||||
│ ├── (private-routes)/
|
||||
│ │ ├── layout.tsx # 'use client' — wraps PrivateLayout; mounts useSessionRoleSync (hydrates AuthContext roles from /me)
|
||||
│ │ ├── _chrome/SidebarShellSkeleton.tsx # Private (`_`-prefixed, not a route) shared loading.tsx skeleton for the 3 sidebar shells (nurse/admin/partner)
|
||||
│ │ ├── select-role/page.tsx # /select-role — first-use role picker (no public role yet); role router lands here
|
||||
│ │ ├── _chrome/ShellContentSkeleton.tsx # Private (`_`-prefixed, not a route) shared loading.tsx skeleton for nurse/admin/partner — shapes the content area only; MobileShell chrome is already rendered by the enclosing layout
|
||||
│ │ ├── select-role/ # /select-role — first-use role picker (no public role yet); role router lands here. Its own layout.tsx wraps FocusedLayout: outside every actor group (no role to tab for) but still inside the phone-width frame
|
||||
│ │ ├── (customer)/ # Customer (family) app — mobile-first, bottom-tab nav; no URL segment
|
||||
│ │ │ ├── layout.tsx # 'use client' — RoleGuard(expected=customer) → CustomerLayout
|
||||
│ │ │ ├── loading.tsx # Route-group loading skeleton (header + search bar + category-tile row + card stack)
|
||||
@@ -163,10 +165,13 @@ client/
|
||||
│ │ │ ├── profile/page.tsx # /profile — ui-phase-9 rebuild into the customer's account hub (still the single route — sub-sections are `FormDialogShell` sheets, not sub-routes): `ProfileSummary` identity header (avatar/initials + name + server-masked phone), grouped tappable rows (اطلاعات شخصی/نشانیها/زبان/اعلانها/پشتیبانی/خروج — the زبان row owns the server-stored `preferredLanguage` and hosts the phase-2 `LocaleSwitcher`, never a second locale mechanism), an emergency-contact status card (tel:-only link when complete, a warm nudge otherwise), `ActorSwitcher` preserved; sign-out goes through `useLogout()` only. No national-ID.
|
||||
│ │ │ ├── support/tickets/ # /support/tickets — f14 "My Tickets" inbox (TicketInboxScreen role="customer") ↔ support/tickets/[id]/page.tsx thread (TicketThreadScreen); thin role-passing wrappers over @/components/messaging
|
||||
│ │ │ └── notifications/page.tsx # /notifications — f14 notification center (NotificationCenter role="customer"); the TopBar bell deep-links here
|
||||
│ │ ├── nurse/ # Nurse app (/nurse/…) — sidebar shell
|
||||
│ │ ├── nurse/ # Nurse app (/nurse/…) — 4-tab bottom nav, one tab per group root
|
||||
│ │ │ ├── layout.tsx # 'use client' — RoleGuard(expected=nurse) → NurseLayout
|
||||
│ │ │ ├── loading.tsx # → ../_chrome/SidebarShellSkeleton
|
||||
│ │ │ ├── page.tsx # /nurse (dashboard) — thin RSC (generateMetadata nav.dashboard) rendering NurseDashboardScreen.tsx
|
||||
│ │ │ ├── loading.tsx # → ../_chrome/ShellContentSkeleton
|
||||
│ │ │ ├── page.tsx # /nurse (the «امروز» tab) — thin RSC (generateMetadata nav.dashboard) rendering NurseDashboardScreen.tsx
|
||||
│ │ │ ├── practice/page.tsx # /nurse/practice — «حرفهٔ من» group root (NursePracticeScreen): a listing-status card (own TrustBadge + the real accepting-bookings state) over links to profile/services/coverage/verification, each with a count read off its already-cached query (omitted, never faked, while one is in flight)
|
||||
│ │ │ ├── finance/page.tsx # /nurse/finance — «مالی» group root (NurseFinanceScreen): the SIGNED net payable balance (an owed-back reads as an error tone, never clamped) over links to earnings/payout history/bank
|
||||
│ │ │ ├── more/page.tsx # /nurse/more — «بیشتر» group root (NurseMoreScreen): ProfileSummary + ActorSwitcher, support/notifications links with unread badges, SettingsPanel, SignOutRow. Everything the sidebar drawer header and footer used to carry
|
||||
│ │ │ ├── NurseDashboardScreen.tsx # ui-phase-7 — the «امروز» operational home replacing the old PlaceholderScreen: greeting+TrustBadge, NextVisitCard (useTodaySessions), RequestsStrip (useNurseRequestInbox, the most time-critical widget — sorts above earnings), EarningsSnapshotCard (useNurseEarningsBalance, signed net + eligible), DashboardActivationSlot, NotificationsEntryRow (useUnreadCount) — every widget is a read of an already-cached query, four-state pattern throughout
|
||||
│ │ │ ├── DashboardActivationSlot.tsx # ui-phase-8 — the named composition point from ui-phase-7's hand-off, now filled with the shared `ActivationChecklist` (same component as `/nurse/services`) instead of the old single-row verification banner
|
||||
│ │ │ ├── requests/ # /nurse/requests — f7 incoming booking-requests inbox, ui-phase-7 redesign: page.tsx = decision-first cards (service+price headline when served — REQ-050, mock-tolerant) + an urgency-tinted CountdownTimer pill (teal/amber/terracotta tiers) + three tabs (در انتظار/پاسخداده merged-client-side page-1-only/منقضی, shared Pager) ↔ requests/[id]/page.tsx detail (only customerNotes + masked city/district; accept now behind a ConfirmDialog + a post-accept payment-window countdown; reject-with-reason invalidate inbox+detail)
|
||||
@@ -187,11 +192,16 @@ client/
|
||||
│ │ │ ├── earnings/ # /nurse/earnings — f12 nurse earnings (read-only), ui-phase-7 pass: page.tsx = EarningsBalanceHeader (net payable balance + 4 buckets, negative "owed back") + a «برداشت بعدی» ForecastLine (server-served only) + an accessible ButtonBase ExplainerCard (aria-expanded, registered `expand` chevron) + state-segmented EarningsRow list (deep-links to /nurse/visits/[id], shared Pager) ↔ payouts/page.tsx (PayoutHistoryRow list) → payouts/[id]/page.tsx (payout/batch reconciliation detail: money decomposition + masked IBAN + booking links); failed-payout reasons now map through `services/payouts/failureReasons.ts` (mapped label headline, raw code demoted to a secondary LTR caption)
|
||||
│ │ │ ├── support/tickets/ # /nurse/support/tickets — f14 nurse "My Tickets" (same TicketInboxScreen/TicketThreadScreen, role="nurse") ↔ support/tickets/[id]/page.tsx
|
||||
│ │ │ └── notifications/page.tsx # /nurse/notifications — f14 notification center (role="nurse"); the nurse-shell bell deep-links here
|
||||
│ │ ├── admin/ # Admin/backoffice (/admin/…) — desktop sidebar shell (f15). Every screen is role-gated via useAdminCapabilities(); the sidebar hides a console the current admin role can't act on (server still enforces). ui-phase-11: every queue page adopts `useAdminListState` (URL-synced filters+page, `@/hooks`) behind a `<Suspense>` wrapper.
|
||||
│ │ ├── admin/ # Admin/backoffice (/admin/…) — 5-tab bottom nav over four group roots (f15). Every screen is role-gated via useAdminCapabilities(); the sidebar hides a console the current admin role can't act on (server still enforces). ui-phase-11: every queue page adopts `useAdminListState` (URL-synced filters+page, `@/hooks`) behind a `<Suspense>` wrapper.
|
||||
│ │ │ ├── layout.tsx # 'use client' — RoleGuard(expected=admin) → AdminLayout (capability-gated nav)
|
||||
│ │ │ ├── loading.tsx # → ../_chrome/SidebarShellSkeleton
|
||||
│ │ │ ├── loading.tsx # → ../_chrome/ShellContentSkeleton
|
||||
│ │ │ ├── page.tsx # Thin RSC — generateMetadata (admin.overview_title) + renders AdminOverviewScreen
|
||||
│ │ │ ├── AdminOverviewScreen.tsx # 'use client' — f15 overview landing: a capability-gated grid of console cards
|
||||
│ │ │ ├── AdminOverviewScreen.tsx # 'use client' — f15 overview landing: every permitted console as a NavHubList, grouped by the same four sections the bottom nav carries (the old 3-column card grid collapsed to one column of icon-and-a-word tiles inside the frame)
|
||||
│ │ │ ├── _hub/AdminGroupHub.tsx # Private (`_`-prefixed, not a route) body every admin group root shares: header + capability-filtered NavHubList + an optional tail; an all-denied group renders an explicit no-access state, never an empty card
|
||||
│ │ │ ├── trust/page.tsx # /admin/trust — «اعتماد» group root (verification queue + review moderation)
|
||||
│ │ │ ├── finance/page.tsx # /admin/finance — «مالی» group root (the weekly payout run)
|
||||
│ │ │ ├── support/page.tsx # /admin/support — «پشتیبانی» group root (ticket queue + internal alerts)
|
||||
│ │ │ ├── system/page.tsx # /admin/system — «سیستم» group root (config/holidays/audit/partners/users/roles) plus the admin identity chip, SettingsPanel and SignOutRow that used to live in the top bar. Always reachable: it is the only way out of the app
|
||||
│ │ │ ├── verification/ # /admin/verification — ui-phase-11 rebuild: page.tsx = status **Tabs** with server counts (when served, REQ-062) + a name/phone search behind the draft-vs-applied Apply/Clear pattern + a client-computed SLA-colored waiting-time column (`WAITING_TIME_WARNING_HOURS`/`_ALARM_HOURS`) ↔ [nurseId]/page.tsx per-nurse case (unchanged DocumentViewer signed-URL docs, pass/reject+reason per step, structured credential entry with `JalaliDateField` issued/expires, Approve enabled only when all steps pass) + «پرونده بعدی/قبلی» next/prev case nav (re-derives the queue's cached page via `queueFilters.ts`) + arrow-key bindings — client never writes is_verified
|
||||
│ │ │ ├── tickets/ # /admin/tickets — ui-phase-11: page.tsx adds an activity column + a results footer ↔ [id]/page.tsx admin thread gains close/reopen/assign-to-me (`useCloseTicket`/`useReopenTicket`/`useAssignTicket`, gated behind `TICKET_LIFECYCLE_ENABLED` + `canManageTickets` — REQ-063, no live route yet), opens scrolled to the newest message (`useThreadScroll`), and the composer turns amber (`--bal-warning-soft`) + relabels its send button in internal-note mode so a note can't be posted publicly by mistake; AdminMessageBubble still renders isInternal notes distinctly; RefundPanel opens from a refund ticket
|
||||
│ │ │ ├── payouts/ # /admin/payouts — ui-phase-11: page.tsx fixes the local-midnight UTC off-by-one on the window default, adopts `JalaliDateField` for the period inputs, and the run-confirm shows the batch total/count/date (from the already-fetched preview) behind `ConfirmDialog`'s new typed-confirmation gate (type «تایید» or the amount) ↔ [batchId]/page.tsx per-nurse rows + failed-payout retry + transfer-reference reconcile, unified onto `PageHeader`
|
||||
@@ -206,11 +216,12 @@ client/
|
||||
│ │ │ └── notifications/page.tsx # /admin/notifications — still a placeholder; NOT in `AdminLayout`'s nav (no real feed yet, ui-phase-10/11) so it satisfies "no placeholder reachable from admin nav"
|
||||
│ │ └── partner/ # Partner-center portal (/partner/…) — a SEPARATE authz scope (f15). A center admin is not a Balinyaar admin; each page resolves the caller's OWN center (useMyPartnerCenter → access-denied on 403/404).
|
||||
│ │ ├── layout.tsx # 'use client' — RoleGuard (no expected role — hydration-only) → PartnerLayout (own partner nav; self-gates via useMyPartnerCenter)
|
||||
│ │ ├── loading.tsx # → ../_chrome/SidebarShellSkeleton
|
||||
│ │ ├── loading.tsx # → ../_chrome/ShellContentSkeleton
|
||||
│ │ ├── page.tsx # Thin RSC — generateMetadata (partner.home_title) + renders PartnerHomeScreen
|
||||
│ │ ├── PartnerHomeScreen.tsx # 'use client' — center home: onboarding/verification state banner + license fields + is_merchant_of_record indicator
|
||||
│ │ ├── nurses/page.tsx # /partner/nurses — the center's sponsored nurses (verification badge)
|
||||
│ │ ├── bookings/ # /partner/bookings — ui-phase-11: page.tsx localizes the 7 booking-status codes onto `StatusChip` (was raw English wire codes, e.g. `pending_payment`) in both the table and filter, adopts `useAdminListState`, and rows link to ↔ [id]/page.tsx (new) — a scoped read-only detail (dates, status timeline via the shared `StatusTimeline`, patient display name only — no clinical data; REQ-064, mock-backed)
|
||||
│ │ ├── more/page.tsx # /partner/more — «بیشتر» group root: the center identity + merchant-of-record status (previously squeezed into the top bar and hidden below `sm`), SettingsPanel, SignOutRow
|
||||
│ │ └── settlement/page.tsx # /partner/settlement — rendered ONLY when is_merchant_of_record: per-booking commission invoices (commission/VAT decomposition via PartnerSettlementRow, signed-URL PDF, masked IBAN); non-MoR shows the "settlement via Balinyaar" state; ui-phase-11 adds a client-side «خروجی CSV» export (`utils/toCsv.ts`, UTF-8 BOM + CRLF for Excel) of the current result set
|
||||
│ ├── (customer-focused)/ # ui-phase-3 — chrome-free counterpart to (customer) for can't-tab-away flows; same URL space (route groups add no segment)
|
||||
│ │ ├── layout.tsx # 'use client' — RoleGuard(expected=customer) → FocusedLayout (no BottomBar/bell/sidebar)
|
||||
@@ -250,6 +261,7 @@ client/
|
||||
│ │ ├── Pager/ # ui-phase-7 — the shared prev/next "page X of Y" control (common namespace i18n) replacing the near-identical inline pagers hand-rolled per list screen (nurse inbox tabs, payout history/earnings) (tested)
|
||||
│ │ ├── InitialsAvatar/ # ui-phase-9 — warm auto-colored initials for a person with no photo: deterministic name-hash → one of 6 `--bal-avatar-*` token pairs (tokens.css, both scheme blocks); `aria-hidden` (decorative next to a visible name); used by `PatientHeader` and (via `ProfileSummary`'s new `initialsFallback` prop) the customer account hub (tested)
|
||||
│ │ ├── FormDialogShell/ # ui-phase-9 — full-screen-below-`sm` form dialog (app-bar header + close) with a dirty-gated discard-confirm on close/backdrop/escape; the shared primitive behind the patient/address add-edit dialogs (the hosted form reports `dirty` via an `onDirtyChange` prop) (tested)
|
||||
│ │ ├── NavHubList/ # the grouped list of destinations a group-root («hub») page is built from — the surface that replaced the sidebar: icon chip + title + one line of orientation + optional badge/meta + chevron, navigating through the locale-aware `@/i18n/navigation` Link (tested)
|
||||
│ │ ├── RouteFadeIn/ # ui-phase-12 — the one route-content fade/slide primitive (motion pass): wraps `{children}`, keyed on the locale-stripped pathname so it remounts (replays the CSS `bal-fade-in` keyframe, globals.css) on navigation but never on an in-place re-render; mounted inside the `ErrorBoundary` in all five shells (tested)
|
||||
│ │ └── index.tsx # barrel — keep next-intl-importing primitives (Money) below the presentational ones so the poisoning risk stays visible in review
|
||||
│ ├── admin/ # f15 backoffice + partner composites (import from `@/components/admin`): AdminDataTable (v2, ui-phase-11 — optional per-column server-param `sort`/`sortable` + `TableSortLabel`, `stickyHeader` scroll viewport, `minWidth`, a `footer` line), AdminPager (page/pageCount; the admin `page_indicator` i18n key regained its `{total}`), AdminPageHeader/AdminEmptyState/AdminErrorState, ConfirmDialog (thin alias), ConfigRow/AuditLogRow (ui-phase-11 — expand chevron rotates + `aria-expanded` + button semantics, new `actorLabel` prop resolved via a batch id→name lookup)/SupportAlertCard (ui-phase-11 — `assignSelfDisabled`/`assignSelfDisabledTitle` so "assign to me" never falls back to a guessed user)/PartnerSettlementRow, DocumentViewer, RefundPanel, AdminMessageBubble, and (ui-phase-11) `UserPicker`/`NursePicker` — async name/phone `Autocomplete` over the admin user directory (REQ-061, mock-backed), replacing every raw numeric-id `TextField` on an audited action; each option renders name+masked-phone+id, never a bare id
|
||||
@@ -298,32 +310,31 @@ client/
|
||||
│ ├── geography/ # F3 geo composites: CascadingRegionSelect, AddressForm, AddressCard (ui-phase-9 added a pin-quality cue — hasPin/pinSetLabel/pinMissingLabel), and the map-pin picker boundary — `AddressMapPicker` now branches on `NESHAN_WEB_KEY` (`@/config`): real Neshan tiles via `NeshanMap` (Leaflet, dynamically imported `ssr:false`; search box + locate-me + draggable pin + reverse-geocoded preview via `services/geography/neshan.ts`'s direct third-party fetch client) when set, else the original bounded-canvas grid stand-in (kept, not deleted, for dev/CI/jsdom) — `{ latitude, longitude }` in/out is identical either way so `AddressForm` never changed (each tested; `NeshanMap` itself isn't unit-tested — jsdom+Leaflet integration — and is unreachable in tests since `NEXT_PUBLIC_NESHAN_KEY` is unset in CI)
|
||||
│ ├── messaging/ # f14 tickets composites (import from @/components/messaging), ui-phase-10 messaging-app rebuild. Screens shared by the customer+nurse pages (role decides chrome): TicketInboxScreen (status filter chips + load-more, EmergencyPlaybookRow instead of a permanent banner), TicketThreadScreen (+ TicketConversationPanel, key={ticketId} — owns the one usePostMessage/draft both TicketMessageList and MessageComposer share, so retry/discard and the composer are one pipeline), TicketMessageList (date separators/author-grouped bubbles/centered system events via useThreadScroll — opens at the newest message, "new message" pill), ContactSupportDialog (new-ticket → shows referenceCode), MessageComposer (controlled; pointer-aware Enter semantics, attachment affordance gated behind `TICKETS_ATTACHMENTS_ENABLED`), BookingSupportEntry (page-local glue on f8 booking detail — reuses the cached booking + care query, no refetch; still mounts the full alarm-red EmergencyBanner, untouched). Pure/tested: MessageBubble (mine/theirs, RTL-mirrored, hh:mm-only, failed-send retry-in-place + discard, `role="alert"` on failure), TicketListCard (prominent referenceCode + unread pill + last-message preview + relative time, mock-tolerant when the enrichment fields are absent), EmergencyBanner (nurse post-confirmation tel: playbook only), EmergencyPlaybookRow (the inbox's compact neutral emergency row). Helpers: statusKind.ts, authorLabel.ts, clientMessageId.ts, useThreadScroll (reusable scroll-orchestration hook, exported for phase 11's admin thread)
|
||||
│ ├── notifications/ # f14 notification composites (import from @/components/notifications), ui-phase-10 pass. NotificationBell (chrome container — subscribes to the polling count so only it re-renders; opens NotificationBellPopover on the nurse desktop shell instead of navigating, everywhere else still navigates) → NotificationBellView (pure, tested, ref-forwarding so the container can anchor the popover), NotificationBellPopover (5-recent preview, fetches on open, exported for phase 11's admin shell once it has a feed), NotificationRow (pure, tested: per-kind tinted icon container, navigable rows get a trailing chevron, non-navigable rows render as a plain non-rippling surface), NotificationCenter (shared page body: unread-first, day-grouped امروز/دیروز/اینهفته with relative timestamps, mark-read-on-open + mark-all, deep-links via notificationDeepLink). Helper: notificationIcon.ts (+ notificationTint). Admin's bell entry is hidden (no real feed yet, `AdminLayout.tsx`) until phase 11 ships one.
|
||||
│ └── auth/ # Auth-flow composites: LoginFlow, PhoneStep, OtpStep, RoleRouter, SelectRole, AuthCard, BrandMark, AuthSplash, RoleGuard (role-aware shell guard, tested), AuthAccountError (/me-failed recovery), useCountdown, useWebOtp (ui-phase-3 WebOTP autofill seam), AuthIllustration + TrustBullets (ui-phase-3 CSS/SVG login-hero treatment)
|
||||
│ ├── settings/ # The app's one appearance-and-language surface (import from @/components/settings): SettingsPanel (language row + the appearance block, tested), ThemeModeSetting (a three-way روشن/تیره/سیستم segmented control, and the ONLY subscriber to useColorScheme() left in the app — an on/off Switch could not express `system`, which is the app's actual default), SignOutRow (always through useLogout()). Mounted in each actor's settings hub — /nurse/more, /admin/system, /partner/more and the customer profile hub — and nowhere else
|
||||
│ └── auth/ # Auth-flow composites: LoginFlow, PhoneStep, OtpStep, RoleRouter, SelectRole, AuthCard (brand mark → step form → divider → TrustBullets on ONE surface; the facts used to float under the card, and the desktop side-illustration went away with the frame), BrandMark, AuthSplash, RoleGuard (role-aware shell guard, tested — also what makes the locale root role-aware), AuthAccountError (/me-failed recovery), useCountdown, useWebOtp (ui-phase-3 WebOTP autofill seam), TrustBullets
|
||||
├── i18n/
|
||||
│ ├── routing.ts # defineRouting — locales: ['en', 'fa'], defaultLocale: 'fa'
|
||||
│ ├── request.ts # getRequestConfig — loads messages/${locale}.json
|
||||
│ └── navigation.ts # ui-2 createNavigation(routing) — Link/usePathname/useRouter/redirect/getPathname. ALL chrome navigation goes through this: usePathname is locale-stripped (so unprefixed ROUTES.* compare directly) and Link/router add the locale automatically — no manual `/${locale}` prefixing, no middleware redirect hop
|
||||
├── layout/ # ui-2 rewrite — per-actor branded chrome + correct locale-aware navigation
|
||||
├── layout/ # the one mobile app shell — per-actor tabs over a shared phone-width frame
|
||||
│ ├── AppFrame.tsx # 'use client' — THE device frame every shell renders inside: a centered `APP_FRAME_MAX_WIDTH` column on a `--bal-frame-canvas` backdrop, header/`<main>`/footer as flex siblings so the frame (not the document) owns the scroll, and `overflowX: hidden` + `minWidth: 0` so an over-wide child clips instead of dragging the app sideways. No shell computes a top offset any more (tested)
|
||||
│ ├── MobileShell.tsx # 'use client' — the ONE authenticated shell behind all four actor apps: AppFrame + a contextual TopBar (brand lockup on a tab's own path, back chevron + route title on anything deeper) + BottomBar + ErrorBoundary + RouteFadeIn + PageTitleProvider. Actors supply only `tabs` + `headerActions`
|
||||
│ ├── PrivateLayout.tsx # authenticated wrapper (passthrough today); actor chrome lives in the shells below
|
||||
│ ├── CustomerLayout.tsx # 'use client' — customer shell: contextual TopBar (brand lockup on the 5 root tabs, title+back on pushed routes) + mobile BottomBar, replaced by an inline desktop top-nav (CustomerDesktopNav) at ≥md; ui-phase-10 added a `useSupportUnreadTotal`-driven Badge on the root-tab support icon (renders only when a signal exists — mock-only until REQ-059)
|
||||
│ ├── NurseLayout.tsx # 'use client' — nurse workspace via TopBarAndSideBarLayout: grouped sidebar (امروز/حرفهٔ من/مالی/پشتیبانی) + ProfileSummary identity card + ActorSwitcher, 5-tab mobile BottomBar («بیشتر» opens the same sidebar drawer); ui-phase-10 added `badgeCount` (useSupportUnreadTotal) on the support sidebar item
|
||||
│ ├── AdminLayout.tsx # 'use client' — admin shell via TopBarAndSideBarLayout: sectioned sidebar (اعتماد/مالی/پشتیبانی/سیستم, useAdminCapabilities-gated, unchanged gating), TopBar identity chip (fine-grained role); no notification bell (ui-phase-10 — admin has no real feed yet, re-add via `NotificationBellPopover` once phase 11 ships one)
|
||||
│ ├── PartnerLayout.tsx # 'use client' — partner portal via TopBarAndSideBarLayout; TopBar identity chip shows the center's own name (useMyPartnerCenter, skeleton while resolving); ui-phase-11 adds a compact merchant-of-record `StatusChip` beside it, persistent across every portal page
|
||||
│ ├── PublicLayout.tsx # unauthenticated shell — minimal corner strip (logo + LocaleSwitcher + dark toggle), no sidebar/bottom bar; AuthCard renders its own larger BrandMark
|
||||
│ ├── FocusedLayout.tsx # ui-phase-3 — chrome-free shell for can't-tab-away flows (today: onboarding): a slim logo strip + content, no BottomBar/bell/sidebar; the route group above it still applies RoleGuard
|
||||
│ ├── TopBarAndSideBarLayout.tsx # 'use client' — the nurse/admin/partner engine: a fixed TopBar (useRouteTitle) + SideBar rendered as flex-row siblings (mobile temporary Drawer + desktop `variant="permanent"` Drawer switched by CSS `sx` breakpoints only — no `useIsMobile` structural branching, so desktop first paint already has the sidebar); optional `identity`/`sidebarIdentity`/`mobileBottomBar` slots
|
||||
│ ├── routeTitle.tsx # ui-2 static route→title map (longest-prefix over ROUTES.*, off the `nav` namespace) + `PageTitleProvider`/`usePageTitleOverride` per-page dynamic-title slot (area phases feed real names in later) + `useRouteTitle`; `isCustomerRootTab`/`CUSTOMER_ROOT_TABS` for the customer header's brand-lockup-vs-title branch
|
||||
│ ├── matchActivePath.ts # ui-2 shared longest-prefix, winner-takes-all active-path matcher (tested) — used by SideBarNavList and BottomBar so a nested route still lights up its parent tab, never a sibling
|
||||
│ ├── config.ts
|
||||
│ ├── CustomerLayout.tsx # 'use client' — customer tabs: خانه (+/search) · رزروها · حلقهٔ مراقبت · کیفپول · پروفایل (+/addresses, /support, /notifications — the account hub that owns appearance/language)
|
||||
│ ├── NurseLayout.tsx # 'use client' — nurse tabs, one per group the sidebar used to hide: امروز (/nurse) · حرفهٔ من (/nurse/practice) · مالی (/nurse/finance) · بیشتر (/nurse/more, `useSupportUnreadTotal` badge). Each group root is a real page; `matchPaths` keeps the historical destination URLs lighting up their group
|
||||
│ ├── AdminLayout.tsx # 'use client' — admin tabs: نمای کلی · اعتماد · مالی · پشتیبانی · سیستم, each a group root. A group tab hides when `useAdminCapabilities` permits nothing inside it (gating unchanged and still per-console; the server still enforces); سیستم is always present because it carries settings + sign-out
|
||||
│ ├── PartnerLayout.tsx # 'use client' — partner tabs: مرکز · پرستاران · رزروها · تسویه · بیشتر. The center identity + merchant-of-record indicator moved from the top bar into /partner/more
|
||||
│ ├── PublicLayout.tsx # unauthenticated shell — the frame and NOTHING else: no top bar, so the login card's own BrandMark is the only mark on screen and the locale/theme toggles that used to sit up here live in settings
|
||||
│ ├── FocusedLayout.tsx # ui-phase-3 — chrome-free framed shell for can't-tab-away flows (onboarding, and /select-role via its own layout.tsx): a slim logo strip + content, no bottom nav; the route group above it still applies RoleGuard
|
||||
│ ├── routeTitle.tsx # ui-2 static route→title map (longest-prefix over ROUTES.*, off the `nav` namespace) + `PageTitleProvider`/`usePageTitleOverride` per-page dynamic-title slot + `useRouteTitle`
|
||||
│ ├── matchActivePath.ts # ui-2 shared longest-prefix, winner-takes-all active-path matcher (tested) — BottomBar runs it over each tab's own path PLUS its `matchPaths` claims, so a nested route still lights up its parent tab, never a sibling
|
||||
│ ├── config.ts # APP_FRAME_MAX_WIDTH (480 — mirrored by components/config.ts's CONTENT_MAX_WIDTH) + TOP_BAR_HEIGHT
|
||||
│ ├── index.ts
|
||||
│ └── components/
|
||||
│ ├── TopBar.tsx # title | titleNode override, align ('start' breadcrumb-style | 'center'), optional secondaryRow (the customer desktop top-nav)
|
||||
│ ├── SideBar.tsx # renders both Drawers (mobile temporary + desktop permanent) off one content tree; close handler wired to the nav list only (dark-mode/locale toggles never close it); brand header + optional identity slot
|
||||
│ ├── SideBarNavList.tsx # renders `ListSubheader` sections when items share a `group`; selection computed once via matchActivePath and passed down
|
||||
│ ├── SideBarNavItem.tsx # navigates via `@/i18n/navigation`'s Link — one navigation, no redirect hop; renders `LinkToPage.badgeCount` as a small Badge on the icon when > 0 (ui-phase-10, the nurse support entry)
|
||||
│ ├── BrandLockup.tsx # ui-2 compact horizontal logo+wordmark — customer header (root tabs) + every sidebar shell's drawer header
|
||||
│ ├── ActorSwitcher.tsx # ui-2 dual customer+nurse session switcher (renders nothing for a single-role session); nurse sidebar + customer profile hub (tested)
|
||||
│ ├── DarkModeButton.tsx # 'use client' — only subscriber to useColorScheme()
|
||||
│ ├── TopBar.tsx # NOT an AppBar — no filled surface, no bottom rule, no elevation: it sits on `background.default` and reads as part of the page rather than a bar covering the top of it. A plain flex row inside AppFrame, never a fixed overlay: title | titleNode override, align ('start' breadcrumb-style | 'center')
|
||||
│ ├── BottomBar.tsx # the app's only navigation surface (tested) — a FLOATING pill bar (inset from the frame edges, `--bal-radius-pill`, `--bal-shadow-2`) rather than an edge-to-edge slab sealing off the bottom; still a flex sibling of the scrolling main, so nothing is ever hidden under it. ButtonBase tabs with an active pill that fills in behind the icon (`--bal-motion-fast`, so the app-wide reduced-motion gate already covers it), `LinkToPage.badgeCount` badges, and `matchPaths`-aware active matching
|
||||
│ ├── BrandLockup.tsx # ui-2 compact horizontal logo+wordmark — the TopBar title on a shell's root tabs
|
||||
│ ├── ActorSwitcher.tsx # ui-2 dual customer+nurse session switcher (renders nothing for a single-role session); nurse «بیشتر» hub + customer profile hub (tested)
|
||||
│ └── index.tsx
|
||||
├── lib/
|
||||
│ ├── api/
|
||||
@@ -664,13 +675,21 @@ reduced-motion branch; extend this one rule if a new motion primitive needs the
|
||||
|
||||
### Toggle components
|
||||
|
||||
`DarkModeToggleButton` and `DarkModeFormSwitch` in `src/layout/components/DarkModeButton.tsx` are the **only** components that subscribe to `useColorScheme()`. When the user toggles:
|
||||
`ThemeModeSetting` in `src/components/settings/ThemeModeSetting.tsx` is the **only** component that subscribes to `useColorScheme()`, and the app's only appearance control. It lives in each actor's settings hub (`/nurse/more`, `/admin/system`, `/partner/more`, the customer profile hub) and nowhere else — the old top-bar toggle spent a permanent slot of chrome in three shells on a preference set once. When the user picks a mode:
|
||||
1. `setMode('dark')` is called
|
||||
2. `Storage.prototype.setItem` intercept fires → writes `'color-scheme'='dark'` cookie synchronously
|
||||
3. MUI sets `data-mui-color-scheme="dark"` on `<html>`
|
||||
4. CSS variables resolve → browser repaints. No React re-render above the button.
|
||||
4. CSS variables resolve → browser repaints. No React re-render above the control.
|
||||
|
||||
Use `colorScheme` (not `mode`) for the `isDark` check — `mode` can be `'system'` even when dark is active.
|
||||
**It is a three-way segmented control (light / dark / system), never a boolean switch.** `system` is
|
||||
the app's real default (`ThemeProvider`'s `defaultMode` on a cookie-less first visit), so an on/off
|
||||
control cannot represent the current state and would silently misreport it.
|
||||
|
||||
Use `colorScheme` (not `mode`) for an `isDark` check — `mode` can be `'system'` even when dark is
|
||||
active. The one exception is the control itself, which must read `mode`: that is the user's *choice*,
|
||||
while `colorScheme` is only the resolved result. `mode` is `undefined` until MUI mounts, so default it
|
||||
(`mode ?? 'system'`) rather than rendering an unselected control — server, first client render and
|
||||
pre-mount state then all agree, so there is no hydration mismatch and no flash of "nothing selected".
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user