19 KiB
Design tokens, theme, typography, brand execution
Current state
The theme lives in client/src/theme/ as a deliberate two-layer token system: tokens.css defines ~30 --bal-* CSS variables (primary/secondary + light/dark/contrast, soft tints, surfaces, text, divider, and four semantic feedback pairs) keyed on [data-mui-color-scheme='light'|'dark'], mirrored by colors.ts (BRAND, LIGHT_PALETTE, DARK_PALETTE) which feeds a MUI v9 cssVariables theme built in theme.ts (createAppTheme produces APP_THEME_LTR/APP_THEME_RTL once at module load). ThemeProvider.tsx wires a direction-aware Emotion cache with stylis-plugin-rtl, CssBaseline enableColorScheme, and a cookie sync so the server (app/[locale]/layout.tsx + lib/cookies/server.ts getThemeMode) stamps data-mui-color-scheme on <html> before first paint for returning visitors. Fonts are loaded per-locale in app/[locale]/layout.tsx: Mikhak (400/500/700 woff2 via next/font/local, preload:false) attached only on fa routes; typography.ts declares a Space Grotesk variable for EN that is explicitly "not currently wired to a font loader". Adoption of the tokens in feature code is exceptionally disciplined — 331 var(--bal-*) usages across 103 component/page files, with raw hexes existing only in theme/colors.ts, one starter SVG, and one test.
What the theme does NOT do is the story: createTheme receives only palette, typography (family+weight only), shape.borderRadius: 10, and direction — there is zero components key anywhere in src (the only styleOverrides grep hit is ErrorBoundary's unrelated text). Every Button, Card, TextField, Chip, Dialog, Table, Tab, and Alert renders stock Material Design with recolored primaries: default elevations, default densities, default focus treatment, Roboto-tuned type metrics. The MUI palette also omits success/error/warning/info, so the 29 files using <Alert severity>/color="success" show stock MUI greens/reds while toasts (lib/toast/NotistackProvider.tsx) use the brand-harmonized --bal-* feedback colors — two visibly different feedback systems. Persian typography inherits MUI's Latin defaults wholesale (rem scale, tight heading line-heights, non-zero letter-spacing that is wrong for joined Persian script), and the "logo" registered in components/common/AppIcon/config.ts is still the starter's multicolor Twemoji pencil SVG, rendered at 56px on the auth screens by components/auth/BrandMark.tsx and as the TopBar logo button. The brand seed deck the tokens were extracted from (product/balinyaar.html) is referenced in tokens.css and .claude/skills/frontend-designer/SKILL.md but no longer exists in the repo. Starter residue persists: dead legacy themes light.ts/dark.ts (built on the deprecated LTR-only TYPOGRAPHY export, exported from theme/index.ts including LIGHT_THEME as default — never imported by the app), a raw starter globals.css, and starter-era comments in components/config.ts.
Problems (15)
- [high]
client/src/theme/theme.ts— The MUI theme has NOcomponentscustomization at all — createTheme gets only cssVariables/colorSchemes/typography/shape/direction. No styleOverrides or defaultProps for Button, Card, Paper, TextField, Chip, Dialog, Table, Tabs, Skeleton, etc., so every surface renders stock Material Design (default elevations/shadows, ripple, densities, focus states) with recolored primaries. This is the single biggest reason the app reads as a default-MUI starter instead of the calm/warm brand.- evidence: Lines 19-36: the entire createTheme call — a repo-wide grep for
styleOverrides|defaultPropsfinds only an unrelated hit in ErrorBoundary.tsx
- evidence: Lines 19-36: the entire createTheme call — a repo-wide grep for
- [high]
client/src/components/common/AppIcon/config.ts— The registered brandlogois still the starter's Twemoji multicolor pencil SVG (icons/PencilIcon.tsx with hard-coded fills #D99E82/#EA596E/#FFCC4D/#CCD6DD). It is the logo button in the TopBar shell and is rendered at 56px on every auth screen by components/auth/BrandMark.tsx, where the passedcolor="var(--bal-primary)"prop is silently ignored by the hard-coded fills. The seed-deck logo (deep-teal square, cream glyph, terracotta dot) was never implemented — first-impression brand execution is a cartoon pencil.- evidence: config.ts line 115:
logo: PencilIcon,; BrandMark.tsx line 21:<AppIcon icon="logo" size={56} color="var(--bal-primary)" />
- evidence: config.ts line 115:
- [high]
client/src/theme/colors.ts— LIGHT_PALETTE/DARK_PALETTE never define success/error/warning/info, so all MUI severity surfaces (Alert, Chip color=success, etc. — used in 29 files including checkout, refund status, verification) render stock MUI #2e7d32 green / #d32f2f red, while notistack toasts use the brand-harmonized --bal-success #1f6b50 / --bal-error #a8392a. The same semantic state shows two different color systems depending on whether it arrives as a toast or an inline alert — directly against the skill rule 'Need success/error/warning/info → use --bal-* tokens, not MUI defaults'.- evidence: colors.ts lines 29-75 define only primary/secondary/background/text/divider; AppAlert.tsx defaults
severity='error' variant='filled'to stock MUI error red
- evidence: colors.ts lines 29-75 define only primary/secondary/background/text/divider; AppAlert.tsx defaults
- [high]
client/src/theme/typography.ts— TYPOGRAPHY_RTL sets only fontFamily and weights — the entire Persian type scale is MUI's Roboto-tuned Latin defaults: rem sizes, tight heading line-heights (e.g. h4 1.235) that clip Persian ascenders/descenders, and non-zero letterSpacing on body1/body2/button/caption/overline, which is typographically wrong for joined (cursive) Persian script — letter-spacing visually breaks glyph connections in Mikhak. There is no fa-specific size, line-height, or letter-spacing tuning anywhere, and no responsive heading sizes.- evidence: Lines 43-52: TYPOGRAPHY_RTL contains only fontFamily + fontWeight per variant; no fontSize/lineHeight/letterSpacing overrides exist
- [medium]
client/src/theme/typography.ts— The theme requests fontWeight 600 (h6, button) but the Mikhak loader in app/[locale]/layout.tsx ships only 400/500/700 — CSS font-matching resolves 600 upward to 700, so all intended 'semibold' text renders full Bold in Persian. The same 600-vs-loaded-weights mismatch is repeated in ~68fontWeight: 600sx usages across 42 component/page files, collapsing the weight hierarchy to regular-vs-bold; the loaded Medium 500 is barely used.- evidence: typography.ts lines 38/51 (
fontWeight: 600), layout.tsx lines 40-44 (weights 400/500/700 only); grepfontWeight: 600= 68 hits in 42 files
- evidence: typography.ts lines 38/51 (
- [medium]
client/src/theme/typography.ts— The English brand font is vaporware: BRAND_FONT_VARIABLE_EN '--font-space-grotesk' is declared with a comment admitting it is 'Not currently wired to a font loader; the LTR stack falls back to the system fonts'. /en pages have no brand typeface at all — headings render in Segoe UI/Roboto, so the secondary locale has zero typographic identity.- evidence: Lines 3-5 comment + line 22 DISPLAY_FONT_LTR referencing the never-populated variable
- [medium]
client/src/theme/tokens.css— The token system covers colors only. There are no spacing-scale tokens, no radius steps beyond the single shape.borderRadius:10, no elevation/shadow tokens (all shadows are MUI's default neutral-black stack — cold and grey against the warm cream surfaces), no motion/duration/easing tokens, and no focus-ring token. Focus styling exists exactly once in the whole app, hand-rolled in NurseResultCard.tsx ('&:focus-visible': { outline: '2px solid var(--bal-primary)' }) — keyboard focus everywhere else is MUI's faint default, a real accessibility + polish gap.- evidence: tokens.css lines 20-99 define only color variables; grep
focus-visible|outline:yields a single hit at NurseResultCard.tsx:67
- evidence: tokens.css lines 20-99 define only color variables; grep
- [medium]
client/src/theme/index.ts— Dead starter theme code is still exported as the public API: light.ts/dark.ts build legacy ThemeOptions on the deprecated LTR-only TYPOGRAPHY (system font, no colorSchemes, no cssVariables, no direction) and index.ts exports them plusLIGHT_THEME as default— importing the package default yields a broken, unbranded theme. Only ThemeProvider and getDirection are actually consumed (single import in app/[locale]/layout.tsx). Violates the repo's own no-dead-code rule and is a trap for future contributors.- evidence: index.ts lines 8-17 export APP_THEME/LIGHT_THEME/DARK_THEME/
LIGHT_THEME as default; grep shows the only consumer imports{ ThemeProvider, getDirection }
- evidence: index.ts lines 8-17 export APP_THEME/LIGHT_THEME/DARK_THEME/
- [medium]
client/src/lib/cookies/server.ts— First-visit dark-mode flash: when no color-scheme cookie exists, getThemeMode returnscolorScheme: 'light', so SSR stamps data-mui-color-scheme="light" on <html>; an OS-dark first-time visitor paints the full light theme, then MUI flips to dark client-side (no InitColorSchemeScript/inline pre-paint script). On a cream-vs-deep-teal palette this flash is stark.- evidence: Line 46:
return { colorScheme: 'light', defaultMode: 'system' };combined with layout.tsx line 89data-mui-color-scheme={colorScheme}
- evidence: Line 46:
- [medium]
client/src/theme/tokens.css— The brand source of truth is missing: tokens.css says the palette was 'extracted from the seed-deck proposal (balinyaar.html)' and the frontend-designer skill describes the logo from 'product/balinyaar.html', but that file does not exist anywhere in the repo — the intended identity (logo lockup, imagery, tone) is unrecoverable from the codebase, so gaps between deck and implementation cannot even be checked.- evidence: tokens.css line 4 references balinyaar.html; Glob
**/balinyaar*.htmlacross the repo returns no files
- evidence: tokens.css line 4 references balinyaar.html; Glob
- [low]
client/src/app/globals.css— globals.css is an untouched starter reset with hazards:max-width: 100vw; overflow-x: hidden; max-height: 100vhon html/body (100vw invites scrollbar-width overflow that the hidden overflow then silently masks; max-height:100vh is fragile on mobile browsers vs dvh and makes body scrolling work only by accident) plus a globala { color: inherit; text-decoration: none }that strips native link affordance app-wide. Nothing brandful (selection color, scrollbar, focus) lives here.- evidence: Lines 7-12 and 14-17 — the entire 17-line file
- [low]
client/src/app/[locale]/layout.tsx— Brand metadata is placeholder-grade and not locale-aware: a single static title 'Balinyaar | بالینیار' and description 'Balinyaar web application' serve both /fa and /en (no generateMetadata per locale), and viewport.themeColor is hard-coded to light teal #1d4a40 with no dark-scheme media entry, so dark-mode users get a light-teal browser chrome over a #0f1c19 page.- evidence: Lines 50-58:
themeColor: BRAND.tealanddescription: 'Balinyaar web application'
- evidence: Lines 50-58:
- [low]
client/src/components/config.ts— Starter residue in the component-defaults file: the commentCONTENT_MIN_WIDTH = 320; // CONTENT_MAX_WIDTH - Sidebar widthis factually wrong (800 − 240 = 560) and the whole file keeps the starter's commented-out-alternatives style ('error' // 'error' | 'info' ...), signaling copy-paste config rather than owned design decisions.- evidence: Line 5 comment; lines 10-35
- [low]
client/src/components/common/AppIcon/config.ts— The icon registry (~90 icons) mixes filled and outlined Material styles with no system: legacy starter entries are filled (Home, Settings, Star, AccountCircle, Dashboard, CheckCircle, Cancel, MedicalServices...) while everything added later is deliberately Outlined — adjacent nav/status icons visibly differ in visual weight, reinforcing the default-MUI feel the owner already dislikes. No custom/brand icon set exists (the only custom SVG is the Twemoji pencil).- evidence: Lines 4-33 filled imports vs lines 35-98
*Outlinedimports registered side-by-side in ICONS
- evidence: Lines 4-33 filled imports vs lines 35-98
- [low]
client/src/layout/TopBarAndSideBarLayout.tsx— Physical direction props in the shell (paddingLeft/paddingRightgated onanchor.includes('left')) instead of logical padding — currently rescued at runtime by the stylis-plugin-rtl Emotion cache flipping them, but it contradicts the skill's own RTL rule and couples shell correctness to the RTL cache implementation.- evidence: Lines 53-60 and line 102
Opportunities (10)
- A single MUI components theming pass — the highest-leverage move in the whole app (impact: high, effort: large) — Add a
componentsblock to createAppTheme encoding the brand once: Button (disableElevation, weight, comfortable padding), Paper/Card (hairline divider border + soft teal-tinted shadow instead of grey elevation), TextField (calmer outline, cream-tinted filled variant), Chip (soft --bal-primary-soft/secondary-soft fills), Dialog/Drawer (radius 16, cream surfaces), Tabs (thicker indicator), Table (relaxed density, tinted header), Skeleton (warm tint), Alert (severity colors mapped to --bal-* tokens). Every screen upgrades simultaneously with zero per-page edits — this is the escape hatch from the 'default-MUI starter' look. - Design a real Persian type scale (impact: high, effort: medium) — Replace inherited Roboto metrics in TYPOGRAPHY_RTL with an owned fa scale: letterSpacing: 0 on every variant (joined script), body line-height ≥ 1.7 and heading line-heights ~1.4-1.5 for Persian ascender/descender room, explicit responsive heading sizes (MUI's default 6rem h1 is unusable, which is why no page uses h1/h2 today — grep confirms zero usages), and a weight system built on the actually-loaded 400/500/700 (retire the phantom 600). Wire Space Grotesk via next/font for /en so the secondary locale gets its brand voice.
- Implement the real brand mark and kill the pencil (impact: high, effort: medium) — Build the seed-deck logo (deep-teal rounded square, cream lowercase glyph, single terracotta dot) as a theme-aware SVG component with sizes for TopBar, auth lockup (BrandMark), favicon, and webmanifest/PWA icons. Since product/balinyaar.html is gone, first re-establish the brand source of truth as a product/brand.md (palette, logo construction, tone words, do/don'ts) so design decisions stop living only in a skill file.
- Unify semantic feedback into the MUI palette (impact: high, effort: small) — Add success/error/warning/info (from the existing --bal-* values) into LIGHT_PALETTE/DARK_PALETTE so Alert, Chip, Badge, LinearProgress color props are automatically brand-harmonized, then delete per-component semantic styling. One feedback language across toasts, inline alerts, and status chips — important in a product where refund/verification/payment states are the emotional core.
- Codify a trust-signal design language at token level (impact: high, effort: medium) — Trust IS the product, yet 'verified' has no dedicated visual identity — TrustBadge and verification chips borrow generic primary/success styling. Introduce a
--bal-trust/--bal-trust-softtoken pair (both schemes), a consistent shield/checkmark mark, and a defined 'verified nurse' card treatment (badge placement, tinted ring on avatar, tooltip explaining WHAT was verified: identity, license, Shahkar). This turns the platform's core differentiator into a recognizable, repeatable visual asset instead of an ad-hoc green chip. - Extend tokens beyond color: shadows, radii, motion, focus (impact: medium, effort: medium) — Add teal-tinted elevation tokens (e.g. shadows built on rgba(29,74,64,α) for light / black-teal for dark), a radius scale (4/10/16) documented next to shape.borderRadius, motion tokens (durations + easings) for consistent transitions, and a global :focus-visible ring (2px --bal-primary, offset 2) applied via theme so keyboard accessibility is uniform instead of existing in exactly one card.
- No-flash color-scheme boot + dark browser chrome (impact: medium, effort: small) — Render MUI's InitColorSchemeScript (or a 3-line inline script) before paint so cookie-less OS-dark visitors never see the light flash, and switch viewport.themeColor to the media-query array form ({ media: '(prefers-color-scheme: dark)', color: BRAND.tealDeep }) so the browser UI matches the page in both schemes.
- Delete starter residue from the theme layer (impact: medium, effort: small) — Remove light.ts, dark.ts, the deprecated TYPOGRAPHY export, and the APP_THEME/LIGHT_THEME/default exports from theme/index.ts (nothing imports them); rewrite globals.css as an intentional base (logical-property-safe reset, ::selection in brand teal/cream, dvh-safe heights, focus-visible fallback); fix the false CONTENT_MIN_WIDTH comment and prune the option-menu comments in components/config.ts.
- Commit to one icon style and consider a warmer set (impact: medium, effort: medium) — Normalize the registry to a single style (the Outlined majority) by swapping the ~12 legacy filled starter icons, or go further and adopt a rounded/duotone set (e.g. Material Symbols Rounded or Phosphor) rendered through the existing AppIcon registry — rounded strokes read warmer and less 'admin dashboard', matching clinical-but-human. The name-registry architecture makes this a config-file-only swap.
- Systematize Persian numerals as a component (impact: medium, effort: small) — The Intl plumbing (money.ts, date.ts, booking/format.ts) is correct but every call site must remember the locale parameter; add a tiny // component family (or a useFormatters() hook) so counts, pagination, phone numbers, and durations can never accidentally render Latin digits on fa — and typographic details like the Toman unit label and IRR→Toman display stay consistent.
Keep (do not regress)
- The two-layer token architecture (tokens.css --bal-* variables scheme-keyed on data-mui-color-scheme, mirrored in colors.ts) with the sync rule documented in both file headers — a genuinely well-designed system, keep it as the foundation for any restyle.
- Outstanding token discipline in feature code: 331 var(--bal-*) usages across 103 files and effectively zero hard-coded hexes outside the theme layer (only the starter PencilIcon and one test) — do not let a design pass reintroduce literals.
- Correct MUI v9 RTL setup: dual prebuilt themes (APP_THEME_LTR/APP_THEME_RTL), direction-aware Emotion cache with stylis-plugin-rtl in ThemeProvider.tsx, and lang/dir sourced from the [locale] layout with the documented reasoning for why <html> lives there.
- Cookie-SSR color-scheme sync (lib/cookies/server.ts getThemeMode + data-mui-color-scheme stamped server-side) — returning visitors get zero dark-mode flash; also the documented colorSchemeSelector fix in theme.ts.
- Per-locale font loading done right: Mikhak via next/font/local with preload:false and a conditional className so Persian woff2 never ships to /en, and the skill rule that fonts load only in the locale layout.
- Brand-harmonized feedback tokens plus NotistackProvider styling toasts entirely from --bal-* variables, so toasts track scheme and direction for free through the portal.
- The Persian correctness utility layer: money.ts (BigInt IRR, fa-IR digit grouping, Toman-at-the-boundary), date.ts (fa-IR-u-ca-persian Shamsi via Intl, no date library), text.ts toEnglishDigits for Persian-keyboard input, booking/format.ts locale clocks — this is rare-quality i18n plumbing.
- The few global theme decisions already made are the right ones: shape.borderRadius 10, button textTransform 'none', dark palette that lifts teal to #6fc0ac on deep-teal surfaces instead of inverting to grey.
- The frontend-designer skill (.claude/skills/frontend-designer/SKILL.md) as a written, enforceable design contract — extend it with whatever the design pass adds rather than replacing it.
- The AppIcon name-registry pattern — exactly what makes a future icon-set swap a one-file change.