Files
baya-monorepo/.claude/skills/frontend-designer/SKILL.md
T
2026-07-17 19:05:17 +03:30

19 KiB
Raw Blame History

name, description
name description
frontend-designer 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 — 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 palettesrc/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 variablessrc/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 colortokens.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.
  • 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: 10 (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).
  • 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 (h1h6) 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

  • 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.
  • 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.
  • 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.

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").

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.

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.

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.

Need a new icon: import the *Rounded version 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.


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