manual improvement 1
This commit is contained in:
@@ -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')`, 3–5 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user