Files
baya-monorepo/.claude/skills/frontend-designer/SKILL.md
T
2026-07-27 22:27:04 +03:30

355 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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. This skill is the design contract; the engineering
contract (providers, fetch, cookies, routing) lives in [client/CLAUDE.md](../../../client/CLAUDE.md) — read it
before touching layout/provider/data code, **don't restate it**, and never violate it.
**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.
---
## 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.41.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 | default `severity="error"`, `variant="filled"` |
| `AppImage` | images | wrapper around next/image conventions |
| `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`, `CONTENT_MAX_WIDTH = 800`, `CONTENT_MIN_WIDTH = 320`, alert/link/
loading defaults). Change a default there, not per-call-site.
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
CLAUDE.md "Unit Testing"; wrap with `<ThemeProvider>`, never mock MUI).
---
## 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 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` (`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`) 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 (CLAUDE.md "Constants").
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.
---
## 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 CLAUDE.md).
7. Data/fetch/auth/cookies/toasts → follow CLAUDE.md (`serverFetch`/`clientFetch`,
`@/lib/cookies/*`, `dispatchToast`/`useSnackbar`). Don't reinvent these.
---
## 9. Anti-patterns (design-specific — CLAUDE.md 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]`.
---
## 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` |
| Engineering contract | `client/CLAUDE.md` |