manual improvement 1

This commit is contained in:
hamid
2026-07-27 22:27:04 +03:30
parent bd06ef0016
commit baa3cc63cd
166 changed files with 3111 additions and 1770 deletions
+89 -58
View File
@@ -83,9 +83,15 @@ Colors exist in **two mirrored places** that must stay in sync. Pick the right o
**Beyond color**`tokens.css` also defines non-palette tokens (`colors.ts` never needs
these; they're define-only in CSS):
- **Radius** — `--bal-radius-sm` (4px, controls: buttons/inputs), `--bal-radius-md`
(10px = `theme.shape.borderRadius`, the house default: cards/paper), `--bal-radius-lg`
(16px: dialogs). Reference the token/constant, never invent a new radius.
- **Radius** — `--bal-radius-sm` (6px, controls: buttons/inputs), `--bal-radius-md`
(8px = `theme.shape.borderRadius`, the house default: cards/paper), `--bal-radius-lg`
(12px: dialogs). Reference the token, **never a numeric `sx={{ borderRadius: n }}`**
that multiplies the shape unit, which is how the login card once ended up a 30px pill.
`MuiPaper` pins the md step so a Paper can't drift past it. `--bal-radius-pill` (999px)
is for shapes that genuinely *are* pills — the floating bottom nav, a segmented
control's active chip — never for a card.
- **Frame canvas** — `--bal-frame-canvas`, the backdrop `AppFrame` paints *outside* the
phone-width app column. Never a surface a component draws on.
- **Elevation** — `--bal-shadow-1/2/3`, teal-tinted (black-teal in dark mode) shadow
steps that back `theme.ts`'s `shadows` array — every MUI elevation (Paper, Dialog,
Menu, Popover, AppBar) resolves through these, never MUI's default grey stack.
@@ -105,9 +111,9 @@ these; they're define-only in CSS):
## 3. Typography & fonts
- `shape.borderRadius: 10` (set in `src/theme/theme.ts`) — the house corner radius
- `shape.borderRadius: 8` (set in `src/theme/theme.ts`) — the house corner radius
(= `--bal-radius-md`). Don't override per-component unless deliberate; the radius
*scale* is `--bal-radius-sm` (4, controls) / `-md` (10, cards) / `-lg` (16, dialogs).
*scale* is `--bal-radius-sm` (6, controls) / `-md` (8, cards) / `-lg` (12, dialogs).
- **Weight system — never write `fontWeight: 600`.** Mikhak and Space Grotesk both load
only 400/500/700 (no 600 face), so a requested 600 silently renders full Bold. Use
**700** for headings (`h1``h6`) and buttons/strong emphasis, **500** for lighter
@@ -166,74 +172,99 @@ CLAUDE.md "Unit Testing"; wrap with `<ThemeProvider>`, never mock MUI).
## 5. Layout & page shells
- **Four per-actor shells**, each wrapped in `RoleGuard` (don't touch): `CustomerLayout`
(mobile-first — contextual `TopBar`: brand lockup on the 5 root tabs, title + back
chevron on pushed routes, an inline desktop top-nav at `≥md` replacing the mobile
`BottomBar`), `NurseLayout` / `AdminLayout` / `PartnerLayout` (all three share
`TopBarAndSideBarLayout`, `src/layout/`). All chrome navigation goes through
`@/i18n/navigation` (`Link`/`usePathname`/`useRouter`) — never a raw `next/link` or a
manual `` `/${locale}` `` prefix.
- `TopBarAndSideBarLayout`: a fixed `TopBar` (title from `useRouteTitle()`, the
route→title map in `layout/routeTitle.tsx`) + a `SideBar` rendered as **two Drawers
sharing one content tree** — mobile `temporary` and desktop `variant="permanent"` —
switched purely by `sx` breakpoint, not `useIsMobile()`; the permanent Drawer is a
normal flex sibling of the main column, so desktop reserves its own width with no
manual offset math and no post-hydration layout jump. Optional `identity` (TopBar
chip — admin/partner), `sidebarIdentity` (sidebar card — nurse's `ProfileSummary`),
and `mobileBottomBar` slots.
- Sidebar nav items are `{ title, path, icon, group? }` arrays (`@/utils`'s
`LinkToPage`) built with `useTranslations('nav')`; a shared `group` string on
consecutive items renders a `ListSubheader` section (see `NurseLayout`/`AdminLayout`).
Selection is computed once via the shared `matchActivePath` (longest-prefix,
winner-takes-all) helper — reuse it for any new nav list, never hand-roll
`pathname.startsWith`.
- **Public screens** use `PublicLayout` — a minimal corner strip (logo +
`LocaleSwitcher` + dark toggle), no sidebar/bottom bar; the step content (`AuthCard`)
carries its own larger `BrandMark`, so don't duplicate a big lockup in the shell.
**There is one layout: a phone.** `AppFrame` (`src/layout/AppFrame.tsx`) renders every
screen inside a centered `APP_FRAME_MAX_WIDTH` (480px) column on a `--bal-frame-canvas`
backdrop, at **every viewport**. A wider window gets more canvas, never a wider app —
design one set of states, verify one set of states. Do not add a `≥md` branch that widens
a shell, restores a sidebar, or lays a screen out in columns.
- `AppFrame` owns three structural guarantees, and is the only place any of them is
solved: the width cap; the **frame, not the document, owns the scroll** (header /
`<main>` / footer are flex siblings, so a top bar is `position: static` and no page
needs a top offset); and `overflowX: hidden` + `minWidth: 0`, so an over-wide child
clips rather than dragging the app sideways. Genuinely wide content (a data table)
scrolls **inside its own container** — see `AdminDataTable`'s `TableContainer`.
- **One authenticated shell**: `MobileShell` = `AppFrame` + a contextual `TopBar` (brand
lockup on a tab's own path, back chevron + `useRouteTitle()` on anything deeper) +
`BottomBar` + `ErrorBoundary` + `RouteFadeIn`. The four actor layouts (`CustomerLayout`
/ `NurseLayout` / `AdminLayout` / `PartnerLayout`, each wrapped in `RoleGuard`) supply
only `tabs` and `headerActions`. Add a destination by adding a tab or a hub row — never
by forking the shell.
- **The chrome is light, not structural.** The top bar is *not* an `AppBar` — no filled
surface, no rule, no elevation; it sits on the page background. The bottom bar *floats*:
inset from the frame edges, fully rounded (`--bal-radius-pill`), elevated. Neither should
read as a slab sealing off an edge of a 480px screen.
- **Navigation is the bottom bar. There is no drawer.** Tabs are `LinkToPage` arrays
(`@/utils`) built with `useTranslations('nav')`, 35 of them, and by convention the last
is a settings/«بیشتر» hub. Active state comes from the shared `matchActivePath`
(longest-prefix, winner-takes-all) over each tab's own path **plus its `matchPaths`
claims — use `matchPaths` when a tab owns a destination outside its own URL subtree
(`/nurse/finance` owning `/nurse/earnings`). Never hand-roll `pathname.startsWith`.
- **A nav group's root is a real page**, not a drawer section: a short summary of that
domain (read only off queries that already answer it — never a fabricated figure) over a
`NavHubList` of its destinations. See `/nurse/practice`, `/nurse/finance`,
`/admin/trust`, `/admin/system`.
- **Chrome carries no preferences.** Language and appearance live in `SettingsPanel`
(`@/components/settings`), mounted in each actor's settings hub and nowhere else. The
top bar is for identity, the page title, and at most a notification bell. Appearance is a
three-way segmented control (light/dark/**system**) — never a boolean switch, which cannot
express the app's own default.
- **Public screens** use `PublicLayout` — the frame and nothing else, **no top bar**; the
step content (`AuthCard`) carries the only brand mark on screen. `FocusedLayout` is the
framed chrome-free shell for can't-tab-away flows (onboarding, `/select-role`).
- All chrome navigation goes through `@/i18n/navigation` (`Link`/`usePathname`/
`useRouter`) — never a raw `next/link` or a manual `` `/${locale}` `` prefix. (Inside a
*page*, `AppLink`/`AppButton`'s `to` is a plain `next/link` and still needs the prefix.)
- Page content is auto-wrapped in `ErrorBoundary` inside every shell.
- Shell dimensions are constants in `src/layout/config.ts` (`SIDE_BAR_WIDTH = 240px`,
top-bar `56px` mobile / `64px` desktop). Respect them; don't hard-code.
- Shell dimensions are constants in `src/layout/config.ts` (`APP_FRAME_MAX_WIDTH`,
`TOP_BAR_HEIGHT`). Respect them; don't hard-code.
- A page is `src/app/[locale]/(private|public-routes)/…/page.tsx`. Keep page bodies to
composition + content; push reusable visuals into `src/components/`.
- Constrain reading width with `CONTENT_MAX_WIDTH` (800) for text-heavy views; full-bleed
is fine for dashboards/tables.
- Prefer MUI breakpoints in `sx` (`{ xs: …, md: … }`) for responsive branching over
`useIsMobile()` (`@/hooks`) — the latter is JS/post-hydration and is what caused the
desktop SSR flash `TopBarAndSideBarLayout` now avoids; reach for it only for genuinely
non-structural, JS-only behavior.
- `CONTENT_MAX_WIDTH` mirrors the frame width — a page column can never be wider than the
frame containing it.
- Prefer MUI breakpoints in `sx` for the little responsive branching that remains over
`useIsMobile()` (`@/hooks`) — the latter is JS/post-hydration and caused a real SSR
flash; reach for it only for genuinely non-structural, JS-only behavior.
---
## 6. Icons
Icons are a **name registry**, not free imports. `src/components/common/AppIcon/config.ts`
maps lowercase names → MUI/SVG components. Render with `<AppIcon icon="home" />` or pass
the name to `AppButton`/`AppIconButton` (`icon="search"`).
maps lowercase names → components. Render with `<AppIcon icon="home" />` or pass the name
to `AppButton`/`AppIconButton` (`icon="search"`).
**One visual family: MUI `*Rounded`.** Every registered icon is the `Rounded` variant of
`@mui/icons-material` (warmer, softer strokes than the old filled/outlined mix — fits
"clinical-but-human"). When adding an icon, import the `*Rounded` version; don't mix in a
Filled/Outlined/Sharp/TwoTone icon next to it. ~90 names are registered today, spanning
navigation, catalog, verification, booking, payments, admin, and messaging — read
`AppIcon/config.ts` directly for the full list rather than duplicating it here (it drifts
too fast for a skill doc to track reliably); the two structural rules below don't.
**One visual family: Lucide.** Every registered icon comes from `lucide-react` — a
contemporary outline family on a 24px grid with round caps/joins, which reads far lighter
than the filled glyphs this registry used to carry at the small sizes a phone-width app
actually uses. `@mui/icons-material` is **no longer a dependency**; never reintroduce it.
The house stroke weight is `APP_ICON_STROKE_WIDTH` (1.75 — Lucide ships at 2, which
competes with Mikhak's lighter Persian strokes).
**`size` actually resizes now.** `AppIcon` drives size via `style.fontSize` (the basis for
MUI SvgIcon's internal `1em` sizing) instead of `width`/`height` attributes, which MUI's
own CSS used to beat. `<AppIcon icon="verified" size={48} />` renders 48px — no more
silent 24px flattening.
**The mapping is semantic, not incidental.** A name describes the domain concept
("verification", "earnings", "coverage") and the glyph depicts *that*, so swapping the
underlying glyph never leaks into call sites. Related concepts share a visual root on
purpose: trust/verification names are shields, money names are coins or cards, clinical
names are a pulse or a cross. ~110 names are registered — read `AppIcon/config.ts` for the
list rather than duplicating it here; the structural rules below are what won't drift.
**`size` drives real `width`/`height`.** Lucide sizes off SVG attributes, so
`<AppIcon icon="verified" size={48} />` is 48px with no `fontSize`/`1em` indirection.
Icons also default to `flexShrink: 0` — an icon squashed by a flex sibling was the one
layout bug this component kept quietly reintroducing on narrow rows.
**Directional icons mirror automatically.** Icons authored for LTR that must flip under
RTL (`back`, `chevron_start`) are registered in `AppIcon/config.ts`'s `DIRECTIONAL_ICONS`
set. `AppIcon` stamps `data-icon-directional` on those, and one CSS rule
(`app/globals.css`) does `[dir='rtl'] [data-icon-directional] { transform: scaleX(-1); }`.
Adding a new directional icon is a one-line registry addition — never hand-roll a
per-component flip.
RTL (`back`, `chevron_start`, `chevron_end`) are registered in `AppIcon/config.ts`'s
`DIRECTIONAL_ICONS` set. `AppIcon` stamps `data-icon-directional` on those, and one CSS
rule (`app/globals.css`) does
`[dir='rtl'] [data-icon-directional] { transform: scaleX(-1); }`. Adding a new directional
icon is a one-line registry addition — never hand-roll a per-component flip.
**Need a new icon:** import the `*Rounded` version into `config.ts`, add a **lowercase**
**Need a new icon:** import it from `lucide-react` into `config.ts`, add a **lowercase**
key to `ICONS`, then reference by that name. Custom SVGs (the brand mark) go in
`AppIcon/icons/`. An unregistered name logs a dev-only warning and falls back to
`default` — never pass a raw MUI icon where a name is expected.
`AppIcon/icons/` and must accept the same `size`/`color`/`strokeWidth` contract
(`AppIcon/utils.ts`'s `IconProps`). An unregistered name logs a dev-only warning and falls
back to `default` — never pass a raw icon component where a name is expected.
---