19 KiB
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 monochromecurrentColorversion of just the glyph (no ground square), registered asICONS.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) forBrandMark(auth splash). The wordmark next to it stays real, translated<Typography>— never bake locale text into an SVG.- The favicon (
src/app/favicon.ico) andpublic/img/favicon/*.pngare 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 asharp-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:
-
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. -
--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 asvar(--bal-primary),var(--bal-success-contrast), etc.
Rules:
- Styling a MUI component → use palette keys (
color="primary",sxpalette 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), thenvar(--…). Never hard-code a hex insx,styled, or a component. - Adding/changing a color means editing
tokens.cssandcolors.tstogether (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(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 backtheme.ts'sshadowsarray — 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 ringMuiCssBaseline's global:focus-visibleoverride uses. Don't hand-roll a focus style; it's already uniform everywhere. - Rating —
--bal-rating/--bal-rating-empty(filled/empty star colors) —RatingInputuses these, not--bal-warning. - Trust —
--bal-trust/--bal-trust-soft, a distinct identity (not primary/success) for verified marks —TrustBadgeand 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 insrc/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 (h1–h6) and buttons/strong emphasis, 500 for lighter in-text emphasis (subtitles, row labels, chip text). This is enforced globally intypography.ts; match it in any newsxyou write. - Buttons:
textTransform: 'none', weight 700 (set globally intypography.ts). Never re-uppercase button text. - Persian type scale (
TYPOGRAPHY_RTL):letterSpacing: 0on 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), andresponsiveFontSizes()wraps both themes (theme.ts) so heading sizes scale down on small viewports — don't hand-roll per-breakpointfontSizeoverrides. - Fonts are loaded per-locale in
src/app/[locale]/layout.tsxonly — Mikhak (--font-mikhak) forfa, Space Grotesk (--font-space-grotesk, vianext/font/google, self-hosted at build time) foren. Bothpreload: falsewith a conditional.variableclassName 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 — contextualTopBar: brand lockup on the 5 root tabs, title + back chevron on pushed routes, an inline desktop top-nav at≥mdreplacing the mobileBottomBar),NurseLayout/AdminLayout/PartnerLayout(all three shareTopBarAndSideBarLayout,src/layout/). All chrome navigation goes through@/i18n/navigation(Link/usePathname/useRouter) — never a rawnext/linkor a manual`/${locale}`prefix. TopBarAndSideBarLayout: a fixedTopBar(title fromuseRouteTitle(), the route→title map inlayout/routeTitle.tsx) + aSideBarrendered as two Drawers sharing one content tree — mobiletemporaryand desktopvariant="permanent"— switched purely bysxbreakpoint, notuseIsMobile(); 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. Optionalidentity(TopBar chip — admin/partner),sidebarIdentity(sidebar card — nurse'sProfileSummary), andmobileBottomBarslots.- Sidebar nav items are
{ title, path, icon, group? }arrays (@/utils'sLinkToPage) built withuseTranslations('nav'); a sharedgroupstring on consecutive items renders aListSubheadersection (seeNurseLayout/AdminLayout). Selection is computed once via the sharedmatchActivePath(longest-prefix, winner-takes-all) helper — reuse it for any new nav list, never hand-rollpathname.startsWith. - Public screens use
PublicLayout— a minimal corner strip (logo +LocaleSwitcher+ dark toggle), no sidebar/bottom bar; the step content (AuthCard) carries its own largerBrandMark, so don't duplicate a big lockup in the shell. - Page content is auto-wrapped in
ErrorBoundaryinside every shell. - Shell dimensions are constants in
src/layout/config.ts(SIDE_BAR_WIDTH = 240px, top-bar56pxmobile /64pxdesktop). 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 intosrc/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 overuseIsMobile()(@/hooks) — the latter is JS/post-hydration and is what caused the desktop SSR flashTopBarAndSideBarLayoutnow 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:
- i18n — no hard-coded user-facing strings. Add the key to both
messages/en.jsonandmessages/fa.json(keep them in sync; top-level keys are namespaces). Client:useTranslations(ns). Server:getTranslations(ns). - RTL-safe.
fais 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; prefermarginInlineStart,start/end,sxshorthand that the RTL Emotion cache mirrors). Test the layout visually at/fa. Do not passflexWrap/useFlexGapasStackprops — usesx={{ flexWrap: 'wrap' }}. - 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. - Tokens, not hexes. No raw color literals in
sx/styled/components (§2). - Constants, not magic values. Cookie names, routes, repeated dimensions, event names → named constants (CLAUDE.md "Constants").
- Use the wrappers (§4) and the icon registry (§6) before bare MUI.
- Shared component ⇒ co-located test (§4).
- MUI v9 API only. No v5/v6-era props (e.g.
StackuseFlexGap,storageWindow). Avoid deprecated APIs that throw.
8. Workflow: turning a design or feature into a screen
- Locate & scope. Decide private vs public route; confirm the layout shell. Identify
which existing
App*components and tokens already cover the design. - Tokens first. If the design needs a color/spacing not in the system, add the
--bal-*token (both schemes) + mirror incolors.tsif it's palette-level. - Compose with primitives. Build with
Box/Stack/Grid/Paper+App*wrappers. Keep raw MUI to layout/structural components. - Wire copy through i18n. Every label/placeholder/aria string → both message files.
- Verify the four axes:
/fa(RTL) and/en(LTR) × light and dark. The default route is/fa— start there. - Tests for any new shared component; never add a layout above
[locale](breaks locale/dir — see CLAUDE.md). - 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 → useAPP_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 |