429 lines
27 KiB
Markdown
429 lines
27 KiB
Markdown
---
|
||
name: frontend-designer
|
||
description: >-
|
||
Design and build UI for the Balinyaar client (Next.js 16 + MUI v9). Use when
|
||
creating or restyling any screen, page, component, layout, or visual in
|
||
client/ — turning a feature, mockup, or Figma design into branded, RTL-aware,
|
||
dark-mode-ready, i18n-complete React/MUI code. Covers the brand palette,
|
||
design tokens, typography, the App* component library, layout shells, icons,
|
||
and the hard rules every Balinyaar UI must follow.
|
||
---
|
||
|
||
# Balinyaar Frontend Designer
|
||
|
||
Build UI that looks like Balinyaar and behaves correctly in both locales and both
|
||
color schemes on the first try.
|
||
|
||
**Precedence.** This skill is the **design** contract — brand, tone, and the visual
|
||
decisions. The **engineering** contract is [client/CLAUDE.md](../../../client/CLAUDE.md)
|
||
(hard rules) plus [docs/rules/client/](../../../archive/docs/rules/client/) (one reference file
|
||
per area). Where the two overlap — tokens, typography, the component library, shells,
|
||
icons — **`docs/rules/client/` is authoritative and this skill defers to it.** Read the
|
||
relevant one before touching layout, provider, or data code; don't restate it here, and
|
||
never violate it.
|
||
|
||
| For | Read |
|
||
|-----|------|
|
||
| Tokens, palette, dark mode, RTL, fonts, motion | [docs/rules/client/theme.md](../../../archive/docs/rules/client/theme.md) |
|
||
| The `App*` library, shells, navigation, icons | [docs/rules/client/components.md](../../../archive/docs/rules/client/components.md) |
|
||
| Copy and Persian orthography | [docs/rules/client/i18n.md](../../../archive/docs/rules/client/i18n.md) |
|
||
| Forms | [docs/rules/client/forms.md](../../../archive/docs/rules/client/forms.md) |
|
||
|
||
**Stack:** Next.js 16 (App Router, Turbopack) · React 19 · MUI v9 (`@mui/material`) ·
|
||
Emotion (RTL via `stylis-plugin-rtl`) · next-intl v4 · notistack. Everything below
|
||
lives under `client/src/`.
|
||
|
||
---
|
||
|
||
## 1. Brand identity
|
||
|
||
Balinyaar is a **trust-first home-nursing marketplace in Iran**. The visual tone is
|
||
calm, warm, clinical-but-human — not a cold medical dashboard. Default audience is
|
||
Persian (RTL); English is secondary.
|
||
|
||
**Logo mark** — this skill is the construction source of truth (the original
|
||
`product/balinyaar.html` seed deck no longer exists in the repo): a deep-teal
|
||
rounded-square ground (`var(--bal-primary)`), a cream lowercase "b" glyph built
|
||
from a stem + a ring bowl (`var(--bal-primary-contrast)`), and a single
|
||
terracotta dot accent (`var(--bal-secondary)`). That trio — **teal ground, cream
|
||
glyph, terracotta accent** — is the whole identity. Use terracotta sparingly as
|
||
the single accent; teal carries everything else. Implemented as two SVGs under
|
||
`components/common/AppIcon/icons/`:
|
||
- `LogoMark.tsx` — a monochrome `currentColor` version of just the glyph (no
|
||
ground square), registered as `ICONS.logo`. Use via `<AppIcon icon="logo">`
|
||
anywhere an inline, recolorable brand glyph is needed.
|
||
- `LogoLockup.tsx` — the full-color mark (ground + glyph + dot, token-driven so
|
||
it tracks the color scheme) for `BrandMark` (auth splash). The wordmark next
|
||
to it stays real, translated `<Typography>` — never bake locale text into an SVG.
|
||
- The favicon (`src/app/favicon.ico`) and `public/img/favicon/*.png` are
|
||
rasterized from the same construction (fixed brand hex, not CSS vars — static
|
||
binary assets are the one place a literal hex is correct). Regenerate with a
|
||
`sharp`-based script if the mark ever changes; don't hand-edit the PNGs/ICO.
|
||
|
||
| Role | Light | Dark |
|
||
|------|-------|------|
|
||
| Primary — deep teal | `#1d4a40` | `#6fc0ac` (lifted, readable on dark) |
|
||
| Secondary — terracotta | `#d98c6a` | `#e6a98a` |
|
||
| Page surface | `#faf9f5` cream | `#0f1c19` deep teal |
|
||
| Paper / card | `#ffffff` | `#16302a` teal surface |
|
||
| Text primary | `#1b2521` ink | `#f3efe9` cream |
|
||
|
||
---
|
||
|
||
## 2. Design tokens — the two-layer system (read this before styling anything)
|
||
|
||
Colors exist in **two mirrored places** that must stay in sync. Pick the right one:
|
||
|
||
1. **MUI palette** — `src/theme/colors.ts` (`BRAND`, `LIGHT_PALETTE`, `DARK_PALETTE`).
|
||
Drives `--mui-palette-*` and all MUI component coloring. Reach it through MUI APIs:
|
||
`color="primary"`, `sx={{ color: 'text.secondary', bgcolor: 'background.paper' }}`.
|
||
**This is the default for component styling.** It auto-switches with the color scheme.
|
||
|
||
2. **`--bal-*` CSS variables** — `src/theme/tokens.css`, defined under
|
||
`[data-mui-color-scheme='light'|'dark']`. The source of truth for **custom CSS
|
||
outside MUI's palette** and for **semantic feedback colors MUI doesn't define**:
|
||
`--bal-success`, `--bal-error`, `--bal-warning`, `--bal-info` (each `+ -contrast`).
|
||
Reference as `var(--bal-primary)`, `var(--bal-success-contrast)`, etc.
|
||
|
||
**Rules:**
|
||
- Styling a MUI component → use palette keys (`color="primary"`, `sx` palette refs).
|
||
- Need success/error/warning/info → use `--bal-*` tokens, **not** MUI defaults — the
|
||
MUI palette has no semantic colors, and these tokens are brand-harmonized.
|
||
- Need a custom color in raw CSS → add a `--bal-*` token (in **both** scheme blocks),
|
||
then `var(--…)`. **Never hard-code a hex** in `sx`, `styled`, or a component.
|
||
- Adding/changing a color means editing `tokens.css` **and** `colors.ts` together (the
|
||
file headers call out the sync requirement).
|
||
|
||
**Beyond color** — `tokens.css` also defines non-palette tokens (`colors.ts` never needs
|
||
these; they're define-only in CSS):
|
||
- **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.
|
||
- **Motion** — `--bal-motion-fast/base/slow` (120/200/300ms) + `--bal-easing-standard`.
|
||
Consumed by the phase-12 app-wide motion pass; use them for any transition you add now.
|
||
- **Focus** — `--bal-focus-ring`, the 2px ring `MuiCssBaseline`'s global `:focus-visible`
|
||
override uses. Don't hand-roll a focus style; it's already uniform everywhere.
|
||
- **Rating** — `--bal-rating` / `--bal-rating-empty` (filled/empty star colors) —
|
||
`RatingInput` uses these, not `--bal-warning`.
|
||
- **Trust** — `--bal-trust` / `--bal-trust-soft`, a distinct identity (not
|
||
primary/success) for verified marks — `TrustBadge` and any future verification UI.
|
||
- **Money emphasis** — `--bal-money-emphasis`, an AA-contrast-safe color for emphasized
|
||
money text. `--bal-secondary` (terracotta) fails AA contrast at small sizes on light
|
||
backgrounds — never use it for money text, use this token instead.
|
||
- **Soft fills** — every brand and semantic color has a `-soft` variant
|
||
(`--bal-primary-soft`, `--bal-warning-soft`, …) for a tinted background. Reach for it
|
||
before hand-mixing an alpha over a surface.
|
||
- **Avatar** — `--bal-avatar-1..6` (+ each `-contrast`), the six warm pairs
|
||
`InitialsAvatar` picks from by a deterministic name hash. Add a seventh to **both**
|
||
scheme blocks or don't add one.
|
||
- **Map** — `--bal-pin-shadow`, the address-picker pin.
|
||
|
||
Full catalogue, with what each group backs:
|
||
[docs/rules/client/theme.md](../../../archive/docs/rules/client/theme.md) §2.
|
||
|
||
---
|
||
|
||
## 3. Typography & fonts
|
||
|
||
- `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` (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
|
||
in-text emphasis (subtitles, row labels, chip text). This is enforced globally in
|
||
`typography.ts`; match it in any new `sx` you write.
|
||
- Buttons: `textTransform: 'none'`, weight 700 (set globally in `typography.ts`). Never
|
||
re-uppercase button text.
|
||
- Persian type scale (`TYPOGRAPHY_RTL`): `letterSpacing: 0` on every variant (Persian is
|
||
a joined script — tracking breaks glyph connections), body line-height ≥1.7, heading
|
||
line-height ~1.4–1.5 (room for ascenders/descenders), and `responsiveFontSizes()`
|
||
wraps both themes (`theme.ts`) so heading sizes scale down on small viewports —
|
||
don't hand-roll per-breakpoint `fontSize` overrides.
|
||
- **Fonts are loaded per-locale in `src/app/[locale]/layout.tsx` only** — Mikhak
|
||
(`--font-mikhak`) for `fa`, **Space Grotesk** (`--font-space-grotesk`, via
|
||
`next/font/google`, self-hosted at build time) for `en`. Both `preload: false` with a
|
||
conditional `.variable` className so neither ships to the other locale.
|
||
**Never load a font in a component or page.**
|
||
- Use `<Typography variant=…>` for text — it inherits the correct direction-aware family
|
||
(`TYPOGRAPHY_RTL` = Mikhak everywhere for full Persian glyph coverage; `TYPOGRAPHY_LTR`
|
||
= Space Grotesk headings + system-stack body). Import neither directly in components;
|
||
let the theme apply them.
|
||
|
||
---
|
||
|
||
## 4. The component library — reach for these before raw MUI
|
||
|
||
Shared primitives live in `src/components/` (barrel: `@/components`). Prefer the `App*`
|
||
wrapper over the bare MUI component — the wrappers carry the house defaults.
|
||
|
||
| Component | Use for | Notes |
|
||
|-----------|---------|-------|
|
||
| `AppButton` | all buttons & button-links | default `variant="contained"`; pass `to`/`href` to render as a link automatically; `startIcon`/`endIcon` accept an **icon name string** (e.g. `startIcon="search"`) or a node; non-MUI `color` strings become text color |
|
||
| `AppIconButton` | icon-only actions | takes an icon `name`, `title`, `to`/`onClick` |
|
||
| `AppIcon` | any icon | `icon="home"` by registered name (§6); `size`, `color` props |
|
||
| `AppLink` | internal/external links | locale-aware Next navigation; default underline `hover` |
|
||
| `AppAlert` | inline alerts | defaults to a calm `severity="info"`, `variant="standard"` — pass `severity="error"` explicitly when it really is an error |
|
||
| `AppLoading` | loading state | default circular, `primary`, `3rem` |
|
||
| `ErrorBoundary` | wrapping fault-prone subtrees | already wraps page content in the shell |
|
||
| `ProfileSummary` | the identity card in chrome | avatar+name+masked phone+role label+optional `TrustBadge`; vertical or `compact` horizontal chip |
|
||
|
||
Defaults for these live in `src/components/config.ts` (`APP_BUTTON_VARIANT`,
|
||
`APP_ICON_SIZE = 24`, `APP_ICON_STROKE_WIDTH = 1.75`, `APP_BUTTON_ICON_SIZE = 20`,
|
||
`CONTENT_MAX_WIDTH = 480`, `CONTENT_MIN_WIDTH = 320`, alert/link/loading defaults).
|
||
Change a default there, not per-call-site.
|
||
|
||
Beyond the `App*` wrappers there is a **state kit** — `EmptyState`, `ErrorState`,
|
||
`QueryStateGate`, `PageHeader`, `ConfirmDialog`, `SurfaceCard`, `AccentCard`, `Money`,
|
||
`StatusTimeline`, the `Jalali*` date inputs, `StickyActionBar`, `Pager`, `NavHubList`,
|
||
`InitialsAvatar`, `FormDialogShell` — with **one pattern per state**. Never hand-roll a
|
||
dashed-border "nothing here" block or a per-screen pager; and **an error state is never an
|
||
empty state.** Catalogue in
|
||
[docs/rules/client/components.md](../../../archive/docs/rules/client/components.md).
|
||
|
||
For layout/spacing use MUI primitives directly: `Box`, `Stack`, `Container`, `Grid`,
|
||
`Paper`, `Card`. Use the `spacing`/`sx` system (theme spacing unit = 8px) — never inline
|
||
pixel margins for rhythm.
|
||
|
||
**New shared component?** Put it in `src/components/<Name>/<Name>.tsx` with an
|
||
`index.tsx` barrel, follow the `App*` prop-spreading + JSDoc style of `AppButton.tsx`,
|
||
and add a co-located `.test.tsx` (mandatory for anything imported in >1 place — see
|
||
[docs/rules/client/testing.md](../../../archive/docs/rules/client/testing.md); wrap with
|
||
`<ThemeProvider>`, never mock MUI). If it goes at the top of the `@/components/common`
|
||
barrel, prefer **caller-owned copy** (required `title`/`body`/`retryLabel` string props)
|
||
over calling `useTranslations` inside it — `next-intl` is ESM-only and poisons every test
|
||
that transitively imports the barrel. `ErrorBoundary`/`ErrorState` are the model;
|
||
[components.md](../../../archive/docs/rules/client/components.md) has the why.
|
||
|
||
**Any form with more than one field is a react-hook-form form**, bound through the
|
||
`@/components/common/form` wrappers (`RhfTextField`, `RhfChipSelect`,
|
||
`RhfJalaliDateField`, `RhfControlGroup`) and grouped into `FormSection`s. A single-field
|
||
control is state, not a form. Full pattern:
|
||
[docs/rules/client/forms.md](../../../archive/docs/rules/client/forms.md).
|
||
|
||
---
|
||
|
||
## 5. Layout & page shells
|
||
|
||
**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 four structural guarantees, and is the only place any of them is
|
||
solved: the width cap; the **frame, not the document, owns the scroll** (a single
|
||
scrolling `<main>` fills the frame, with the bars pinned **`position: absolute`** over
|
||
it — never `fixed`, which would break out of the centered column — and `<main>`
|
||
reserving each bar's exact height as padding, so no page needs a top offset);
|
||
`overflowX: hidden` + `minWidth: 0`, so an over-wide child clips rather than dragging
|
||
the app sideways; and, above `sm`, the column **floats** as a rounded shadowed card
|
||
with a gutter all round (edge-to-edge on a phone). Genuinely wide content (a data
|
||
table) scrolls **inside its own container** — see `AdminDataTable`'s `TableContainer`.
|
||
- `AppFrame` also publishes **`--bal-chrome-top` / `--bal-chrome-bottom`** on the scroll
|
||
container (already including `env(safe-area-inset-*)`, and `0px` in a chrome-free
|
||
shell), so any `position: sticky` element can clear the bars without importing a
|
||
constant. `StickyActionBar` is the reference consumer — don't recompute an offset.
|
||
- **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 of its own; `AppFrame` wraps both bars in the shared
|
||
`FLOATING_BAR_SX`, so the header is the bottom bar mirrored: 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. The bottom bar is **icon-only** (at five tabs the caption was
|
||
the widest thing in it and cost a whole line — the label survives as `aria-label`/
|
||
`title`), each tab a fixed 44px circle laid out `space-around`.
|
||
- **A stateful card carries its state in its content, not a stripe.** `AccentCard`'s
|
||
colored edge stripe was removed — a column of them read as a row of loose vertical rules
|
||
down the RTL edge of the screen. `tone` survives as the semantic label (reaching the DOM
|
||
as `data-accent-tone`); the `StatusChip`, icon and copy inside carry the state.
|
||
**Do not reintroduce the stripe.**
|
||
- **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` (`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/`.
|
||
- `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 → components. Render with `<AppIcon icon="home" />` or pass the name
|
||
to `AppButton`/`AppIconButton` (`icon="search"`).
|
||
|
||
**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).
|
||
|
||
**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`, `chevron_end`, `forward`, `send`) 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 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/` 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.
|
||
|
||
---
|
||
|
||
## 7. Non-negotiable rules for every Balinyaar UI
|
||
|
||
Every screen/component you produce must satisfy **all** of these:
|
||
|
||
1. **i18n — no hard-coded user-facing strings.** Add the key to **both**
|
||
`messages/en.json` and `messages/fa.json` (keep them in sync; top-level keys are
|
||
namespaces). Client: `useTranslations(ns)`. Server: `getTranslations(ns)`.
|
||
2. **RTL-safe.** `fa` is the default locale and is **RTL**. Never use directional
|
||
hard-coding (`marginLeft`, `left:`, `textAlign: 'left'`) for layout flow — use
|
||
logical/MUI-flipped props (`ml`→ MUI flips; prefer `marginInlineStart`, `start`/`end`,
|
||
`sx` shorthand that the RTL Emotion cache mirrors). Test the layout visually at `/fa`.
|
||
Do **not** pass `flexWrap`/`useFlexGap` as `Stack` props — use `sx={{ flexWrap: 'wrap' }}`.
|
||
3. **Dark-mode correct.** Pull every color from the palette or `--bal-*` tokens so it
|
||
switches automatically. Verify on both schemes — never assume a light background.
|
||
4. **Tokens, not hexes.** No raw color literals in `sx`/`styled`/components (§2).
|
||
5. **Constants, not magic values.** Cookie names, routes, repeated dimensions, event
|
||
names → named constants ([components.md](../../../archive/docs/rules/client/components.md) §5).
|
||
6. **Use the wrappers** (§4) and the **icon registry** (§6) before bare MUI.
|
||
7. **Shared component ⇒ co-located test** (§4).
|
||
8. **MUI v9 API only.** No v5/v6-era props (e.g. `Stack` `useFlexGap`, `storageWindow`).
|
||
Avoid deprecated APIs that throw.
|
||
9. **Persian copy follows the style guide** — «بالینیار» with a ZWNJ, تأیید with a hamza,
|
||
جستجو in one form, formal شما. `npm run lint:copy` fails the gate on a banned variant.
|
||
Glossary and the full rules: [i18n.md](../../../archive/docs/rules/client/i18n.md) §4.
|
||
10. **A screen never fabricates a figure.** A summary reads only off a query that already
|
||
answers it; a count still in flight is omitted, never faked or defaulted.
|
||
|
||
---
|
||
|
||
## 8. Workflow: turning a design or feature into a screen
|
||
|
||
1. **Locate & scope.** Decide private vs public route; confirm the layout shell. Identify
|
||
which existing `App*` components and tokens already cover the design.
|
||
2. **Tokens first.** If the design needs a color/spacing not in the system, add the
|
||
`--bal-*` token (both schemes) + mirror in `colors.ts` if it's palette-level.
|
||
3. **Compose with primitives.** Build with `Box`/`Stack`/`Grid`/`Paper` + `App*`
|
||
wrappers. Keep raw MUI to layout/structural components.
|
||
4. **Wire copy through i18n.** Every label/placeholder/aria string → both message files.
|
||
5. **Verify the four axes:** `/fa` (RTL) and `/en` (LTR) × light and dark. The default
|
||
route is `/fa` — start there.
|
||
6. **Tests** for any new shared component; **never** add a layout above `[locale]`
|
||
(breaks locale/dir — see [structure.md](../../../archive/docs/rules/client/structure.md)).
|
||
7. Data/fetch/auth/cookies/toasts → follow
|
||
[services.md](../../../archive/docs/rules/client/services.md) and
|
||
[auth.md](../../../archive/docs/rules/client/auth.md) (`serverFetch`/`clientFetch`,
|
||
`@/lib/cookies/*`, `dispatchToast`/`useSnackbar`). Don't reinvent these.
|
||
|
||
---
|
||
|
||
## 9. Anti-patterns (design-specific — `docs/rules/client/` has the full engineering list)
|
||
|
||
- Hard-coded hex/rgb in components → use palette keys or `--bal-*` tokens.
|
||
- MUI default success/error colors for feedback → use `--bal-*` semantic tokens.
|
||
- `marginLeft`/`left`/`textAlign:'left'` for flow → breaks RTL; use logical props.
|
||
- Hard-coded English (or any) UI string → add to both message files.
|
||
- Loading a font in a component/page → fonts live only in `src/app/[locale]/layout.tsx`.
|
||
- `createTheme()` in a component → use `APP_THEME_LTR`/`APP_THEME_RTL`.
|
||
- Raw MUI icon where a registry name is expected → register it in `AppIcon/config.ts`.
|
||
- New shared component without a `.test.tsx`, or mocking MUI in tests.
|
||
- Re-introducing `src/app/layout.tsx` / any layout above `[locale]`.
|
||
- A `≥md` branch that widens a shell, restores a sidebar, or goes multi-column → there is
|
||
one layout, and it is a phone (§5).
|
||
- A numeric `sx={{ borderRadius: n }}` → it multiplies the shape unit; use the radius token.
|
||
- `fontWeight: 600` → neither face loads it, so it silently renders full Bold. 700/500/400.
|
||
- Reintroducing `AccentCard`'s edge stripe, a drawer, a top-bar theme/locale toggle, or a
|
||
caption under a bottom-nav icon → each was deliberately removed.
|
||
- A hand-rolled empty/error/loading block, or a per-screen pager → use the state kit (§4).
|
||
- A second `prefers-reduced-motion` branch → there is exactly one, in `globals.css`.
|
||
|
||
---
|
||
|
||
## 10. Design ↔ Figma (optional)
|
||
|
||
A Figma MCP is connected. When the user provides a figma.com URL or asks to implement a
|
||
Figma frame, use the Figma tools (`get_design_context`, `get_screenshot`, `get_metadata`)
|
||
to pull the design, then map it onto **this** system: translate Figma colors to the
|
||
nearest `--bal-*`/palette token (don't introduce new hexes unless the design truly adds a
|
||
brand color), Figma type to `<Typography>` variants, and Figma components to the `App*`
|
||
library. Follow the Figma plugin skills (`/figma-use`, `/figma-generate-design`) when
|
||
pushing code back into Figma.
|
||
|
||
---
|
||
|
||
## Key files
|
||
|
||
| Concern | File |
|
||
|---------|------|
|
||
| Brand color tokens (CSS) | `client/src/theme/tokens.css` |
|
||
| MUI palette (mirror) | `client/src/theme/colors.ts` |
|
||
| Typography & fonts | `client/src/theme/typography.ts` |
|
||
| Theme factory (shape, schemes, dir) | `client/src/theme/theme.ts` |
|
||
| Theme provider / RTL cache | `client/src/theme/ThemeProvider.tsx` |
|
||
| Component library | `client/src/components/` (`@/components`) |
|
||
| Component defaults | `client/src/components/config.ts` |
|
||
| Icon registry | `client/src/components/common/AppIcon/config.ts` |
|
||
| Layout shells | `client/src/layout/` |
|
||
| Layout dimensions | `client/src/layout/config.ts` |
|
||
| Messages (i18n) | `client/messages/{en,fa}.json` |
|
||
| Persian copy lint | `client/scripts/check-copy.mjs` |
|
||
| Engineering hard rules | `client/CLAUDE.md` |
|
||
| Engineering reference (per area) | `docs/rules/client/` |
|