Files
baya-monorepo/archive/post-phase/ui/ui-phase-0-design-language.md
T
2026-08-02 18:48:32 +03:30

24 KiB
Raw Blame History

UI Phase 0 — Design language & theme foundation

Mission: kill the default-MUI/starter look at the token + theme layer in one pass and establish the brand's visual system — a theme.components pass, a semantic palette, a real Persian type scale, one icon family, a designed Balinyaar logomark, extended tokens (elevation/motion/focus/rating/trust/money), and a purge of starter residue — so every later phase composes on a system that already looks designed. No page redesigns here: this phase changes what every screen inherits, not any screen's layout.

Track: frontend · Depends on: — (first phase of the UI chain) · Unlocks: Phase 1, Phase 2, and every later phase — they all inherit the de-startered look built here. Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the frontend-designer skill — both are mandatory.

1. Context — where this sits

Balinyaar's feature layer is unusually disciplined — 331 var(--bal-*) usages across 103 files, effectively zero hard-coded hexes, correct RTL/Shamsi/BigInt-money plumbing — but the visual system underneath it is still the karpolan react-mui starter. Diagnosed root causes (all verified in code):

  1. createTheme has zero components overridesclient/src/theme/theme.ts:19-36 passes only cssVariables/colorSchemes/typography/shape/direction; every Button/Card/TextField/AppBar/Chip/ Dialog/Alert renders stock Material with recolored primaries. The single biggest cause of the starter feel.
  2. No semantic palettetheme/colors.ts defines only primary/secondary/background/text/divider, so inline <Alert severity> surfaces render stock MUI green/red while toasts (lib/toast/NotistackProvider.tsx) use the brand --bal-* feedback tokens — two feedback systems for one semantic state. Worse, a bare <AppAlert> defaults to a filled stock-red error (components/config.ts:10-11).
  3. Persian typography is Roboto metricsTYPOGRAPHY_RTL (theme/typography.ts:43-52) sets only fontFamily + weights: non-zero letter-spacing on a joined script, tight heading leading, and a requested weight 600 (h6, button) that the Mikhak loader (app/[locale]/layout.tsx:39-48, weights 400/500/700) renders as full Bold — repeated in 68 fontWeight: 600 sx usages across 42 files (verified). Space Grotesk (--font-space-grotesk) is declared for EN but never loaded.
  4. The brand mark is the starter's Twemoji cartoon pencilAppIcon/config.ts:115 registers logo: PencilIcon; components/auth/BrandMark.tsx:21 renders it at 56px on every auth screen and passes color="var(--bal-primary)", which the hard-coded fills (#EA596E, #FFCC4D, …) silently ignore.
  5. The icon system is broken and incoherentAppIcon.tsx:38-46 passes size as SVG width/height attributes, which MUI SvgIcon's 1em class CSS overrides, so every size={14..56} call-site renders 24px. The ~87-entry registry mixes filled starter icons against *Outlined feature icons, carries eight dead entries (zero usages, verified), and has no back/chevron icon at all (only expand).
  6. Starter residue everywhere else: AppButton's DEFAULT_SX_VALUES = { margin: 1 } neutralized by 213 m: 0 occurrences across 82 files; dead theme/light.ts/dark.ts exported from theme/index.ts incl. LIGHT_THEME as default (nothing else imports them); the 17-line starter globals.css; the wrong CONTENT_MIN_WIDTH comment (components/config.ts:5); unused AppImage (all verified); a cookie-less OS-dark first visit paints light first (lib/cookies/server.ts:46 returns 'light', no pre-paint script exists — acknowledged doc drift in client/CLAUDE.md); viewport.themeColor hard-coded to light teal (layout.tsx:50-52); public/site.webmanifest still ships starter #000000/#ffffff and references img/favicon/*.png files that don't exist.

What already exists (do not rebuild):

  • The two-layer token systemtheme/tokens.css (--bal-*, scheme-keyed on data-mui-color-scheme) mirrored by theme/colors.ts, sync rule in both headers. Extend it; never bypass it.
  • The MUI v9 RTL dual-theme setup (APP_THEME_LTR/APP_THEME_RTL built once at module load, stylis-plugin-rtl Emotion cache in ThemeProvider.tsx) and the cookie-SSR color-scheme sync (getThemeMode()data-mui-color-scheme stamped server-side; explicit colorSchemeSelector).
  • Per-locale font loading (Mikhak via next/font/local, preload: false, attached only on fa), the AppIcon string registry with its snake_case domain names (the exact indirection that makes this phase's icon swap a config-level change), brand-styled notistack toasts, and the correct global decisions already made: shape.borderRadius: 10, textTransform: 'none', the lifted dark-teal palette.

2. Required reading (do this first)

  • The audits: audit/theme-and-brand.md, audit/component-primitives.md, audit/cross-cutting-ux.md — the full file/line evidence and the Keep (do not regress) lists this phase must honor.
  • ../../../.claude/skills/frontend-designer/SKILL.md — the design contract you are extending (invoke the skill; this phase also updates it — see 3.10).
  • client/CLAUDE.md — "Golden rules", "Theme System" (the cookie/no-flash machinery and the MUI v9 traps: explicit colorSchemeSelector, never InitColorSchemeScript, never storageWindow), and "Fonts".
  • Code, in this order: all of client/src/theme/, components/config.ts, components/common/AppIcon/ (+ icons/PencilIcon.tsx), components/common/AppButton/AppButton.tsx, components/auth/BrandMark.tsx, lib/toast/NotistackProvider.tsx, app/[locale]/layout.tsx, app/globals.css, lib/cookies/server.ts (getThemeMode), public/site.webmanifest.
  • product/overview/platform-summary.md — the tone (trust-first, calm, clinical-but-human) every visual decision here must serve.

3. Scope — build this

3.1 The theme.components pass (the highest-leverage change in the app)

Add a components block inside createAppTheme (theme/theme.ts) that encodes the brand once. Reference colors as var(--bal-*) tokens (or theme.vars.palette.*) so every override is scheme-correct — never a hex. Overrides must be direction-safe (logical properties only — both theme directions share them). Cover at least: MuiButton (disableElevation, comfortable paddingInline, weight per the 3.3 decision, keep textTransform: 'none'); MuiPaper/MuiCard (hairline 1px solid var(--bal-divider) border + soft teal-tinted shadows from the 3.4 elevation tokens instead of MUI's grey stack on warm cream); MuiAppBar (paper/cream surface + hairline bottom divider, not solid primary — shells are rebuilt in phase 2, this stops the default bar screaming "starter" now); MuiOutlinedInput/MuiTextField (house radius, calmer resting border, brand focus ring); MuiChip / MuiToggleButton(Group) / MuiListItemButton (soft --bal-primary-soft/--bal-secondary-soft tonal fills; selected = primary-soft + primary text — tokens already exist in both schemes); MuiDialog (radius 16, sane mobile margins); MuiTabs (thicker indicator, comfortable min-height); MuiAlert (severities from the 3.2 semantic palette so inline alerts match toasts); MuiStepper/MuiStepIcon (brand-colored active/completed steps — the trust flows live on these); MuiSkeleton (warm tint per scheme); MuiTooltip (ink/cream inversion, house radius); and MuiCssBaseline carrying a global :focus-visible treatment off --bal-focus-ring (2px ring + offset — today exactly one :focus-visible style exists in the app, in NurseResultCard.tsx; after this, keyboard focus is uniform everywhere).

3.2 Semantic palette — one feedback language

  • Add success/error/warning/info (main + contrastText, light and dark) to LIGHT_PALETTE/DARK_PALETTE in colors.ts, sourced from the existing --bal-* feedback values in tokens.css (light: #1f6b50/#a8392a/#8a6418/#1d4a40; dark: the lifted set). Keep the two files mirror-synced. Alert, Chip color="success", Badge, LinearProgress become brand-harmonized automatically.
  • Fix the AppAlert footgun in components/config.ts: first make severity explicit at any call-site that genuinely means error, then change the defaults to a calm baseline (severity="info", variant="standard" on the new tinted severities — decide and document in the config file).

3.3 A real Persian type scale

In theme/typography.ts TYPOGRAPHY_RTL:

  • letterSpacing: 0 on every variant — Persian is a joined script; tracking breaks Mikhak's glyph connections (MUI's Latin defaults set non-zero spacing on body1/body2/button/caption/overline).
  • Body line-height ≥ 1.7; headings ~1.41.5 (room for Persian ascenders/descenders) plus responsive heading sizes (MUI's default 6rem h1 is unusable; grep confirms zero h1/h2 usages today).
  • Resolve the weight-600 problem. Mikhak loads 400/500/700 only, so every requested 600 silently renders 700. Decide: (a) adopt 500/700 semantics — map the theme's h6/button and the 68 fontWeight: 600 sx usages across 42 files to loaded weights (recommended: 700 headings/buttons, 500 in-text emphasis; the sweep is mechanical) — or (b) add a genuine Mikhak DemiBold face to src/app/fonts/ + the loader. Pick one, do it fully, document the decision in typography.ts and the skill (3.10).
  • Settle the EN display font: wire Space Grotesk via next/font in app/[locale]/layout.tsx (attached only on en, mirroring the Mikhak pattern, preload: false) or delete the dead BRAND_FONT_VARIABLE_EN variable and its fallback stack. Decide and do it — no vaporware comment left.

3.4 Token extension — beyond color

Extend theme/tokens.css (both scheme blocks — always), mirroring palette-level values in colors.ts:

  • Elevation/shadow tokens — 23 steps of teal-tinted shadows (light: rgba(29,74,64,α) stacks; dark: black-teal), consumed by 3.1's Paper/Card/Dialog overrides.
  • Radius scale — document the house scale (e.g. 4 / 10 / 16: controls / cards / dialogs) as tokens or constants next to shape.borderRadius; stop inventing radii per component.
  • Motion tokens — durations (e.g. 120/200/300ms) + easings; defined once here, consumed by phase 12 — and --bal-focus-ring, the global focus ring used by 3.1's :focus-visible treatment.
  • --bal-rating + --bal-rating-empty — proper star gold + empty color per scheme (RatingInput's muddy --bal-warning stars are fixed in phase 1 using these); --bal-money-emphasis — a contrast-safe money-text emphasis per scheme, retiring terracotta as a small-text money color (#d98c6a on white fails AA contrast); --bal-trust + --bal-trust-soft — a distinct trust identity for verified marks (not generic primary/success), consumed by phases 1/4/8. All define-only here.
  • Update the stale tokens.css header (it cites product/balinyaar.html, which no longer exists in the repo) to point at the frontend-designer skill as the brand source of truth (3.10).

3.5 A real brand mark — kill the pencil

  • Design the Balinyaar logomark per the identity the skill records (the product/balinyaar.html seed deck is gone from the repo — recreate from the tokens + the skill's description): deep-teal ground, cream lowercase glyph, a single terracotta dot. Build two SVGs under AppIcon/icons/: a monochrome currentColor logomark (so color="var(--bal-primary)" finally works and it recolors in dark mode for free) and a full lockup (mark + wordmark) for BrandMark/auth.
  • Register the mark as ICONS.logo (replacing PencilIcon), update components/auth/BrandMark.tsx to the new lockup, delete icons/PencilIcon.tsx (the last hard-coded-hex SVG in the app), regenerate src/app/favicon.ico from the mark, and fix public/site.webmanifest — brand theme_color/background_color (currently starter #000000/#ffffff) and icon entries that actually exist (generate the referenced PNG sizes from the mark, or trim the manifest — today it points at missing img/favicon/*.png).

3.6 Icon system — one family, working sizes, full vocabulary

  • Fix the size bug in AppIcon.tsx: drive fontSize via style/sx instead of width/height attributes (which MUI SvgIcon's 1em class CSS beats) — all ~40 existing size={14..56} call-sites start working simultaneously. Stop spreading the invalid size attribute onto the DOM <svg>; make the unknown-name console.warn dev-only; keep the custom-SVG path (the new logomark) scaling correctly.
  • Normalize the ~87-entry registry to ONE visual family — recommend the Rounded variants of @mui/icons-material (zero new deps, warmer than the current filled/outlined mix, fits clinical-but-human). A config.ts-only sweep thanks to the registry indirection. Delete the eight dead starter entries (daynight/night/day/visibilityon/visibilityoff/signup/login/settings — re-verify zero usages before each delete).
  • Add the missing vocabulary: back/chevron_start (no directional nav glyph exists today except expand), share, copy, phone (non-emergency call), navigate (directions), sort, attachment, star_half. Note camera, calendar, wallet, and tune (filter) are already registered — they only need family normalization. Directional icons auto-mirror: register back/chevron_start as the LTR glyph, mirrored via a [dir="rtl"] scaleX(-1) rule applied by AppIcon for a declared DIRECTIONAL_ICONS set in config.ts; write the rule into the registry comment and the skill — later phases must not hand-roll flips.

3.7 AppButton de-startering

  • Remove DEFAULT_SX_VALUES (margin: 1), then sweep the now-no-op 213 sx={{ m: 0 }} neutralizations across 82 files (mechanical: remove the key; drop sx when it becomes empty). Outer spacing becomes the parent's job (Stack/Box gaps). Clean the starter prop cruft: duplicate label/text props (pick one, migrate the loser's call-sites), the false "Box around to specify margins" JSDoc, the // Missing props comment, and the underline spread onto non-link buttons (AppButton.tsx:85).
  • Do not break the public API: to/href auto-link composition, icon-name startIcon/endIcon, and non-MUI-color-becomes-text-color all stay (238 call-sites). Leave the color='inherit' default as-is — changing every unspecified button's color is a per-screen decision for later phases. Update AppButton.test.tsx (and every other touched shared component's test) in the same change.

3.8 Starter residue purge

  • Delete theme/light.ts + theme/dark.ts and their exports from theme/index.ts (verified: nothing else imports them; drop LIGHT_THEME/DARK_THEME/APP_THEME/LIGHT_THEME as default — keep ThemeProvider, getDirection, APP_THEME_LTR/RTL). Delete the deprecated TYPOGRAPHY alias in typography.ts once nothing imports it.
  • Rewrite app/globals.css as an intentional minimal base: keep the box-sizing reset, drop max-height: 100vh and the max-width: 100vw; overflow-x: hidden mask, add ::selection in brand colors, and a commented decision on the a { color: inherit } reset (AppLink owns link affordance).
  • Fix the wrong CONTENT_MIN_WIDTH comment (components/config.ts:5), prune the starter commented-out-alternatives style there into owned decisions, and delete the unused AppImage + its test + barrel export (verified: zero product usages).

3.9 No-flash color-scheme boot

  • A cookie-less OS-dark first visit paints light, then flips. Implement the ColorSchemeScript that client/CLAUDE.md's Theme System section already documents (acknowledged doc drift): an inline <head> script in app/[locale]/layout.tsx that reads the color-scheme cookie, falls back to matchMedia('(prefers-color-scheme: dark)') when absent, and sets data-mui-color-scheme before first paint — plus the documented Storage.prototype patch. Never MUI's InitColorSchemeScript (banned in client/CLAUDE.md — it reads localStorage, which diverges from the cookie). The script must agree with getThemeMode() on every path. Switch viewport.themeColor (layout.tsx:50-52) to the media-query array form: light → BRAND.teal, (prefers-color-scheme: dark)BRAND.tealDeep, so browser chrome matches the page in both schemes.

3.10 Keep the design contract honest

Update .claude/skills/frontend-designer/SKILL.md in the same change: the new tokens (elevation, radius, motion, focus, rating, trust, money-emphasis) and when to use each; the type-scale + weight decision (its "buttons weight 600" rule becomes wrong the moment 3.3 lands); the normalized icon family + directional- mirroring rule + a corrected registered-icons note (it lists ~19 of ~87 icons and still names the dead starter entries); and the brand-mark construction as the written source of truth now that product/balinyaar.html is gone (fix its reference; tokens.css's header points here too).

(DEFERRED → phase 1): all new shared primitives — EmptyState/ErrorState, PageHeader, <Money>, Jalali picker, StatusChip v2, RatingInput's star fix, skeleton twins, route-level loading.tsx/error.tsx/404. (DEFERRED → phase 2): the shells — TopBar/SideBar chrome, UserInfo, nav grouping, SSR mobile-first flash. (DEFERRED → phase 12): the app-wide motion pass that consumes 3.4's motion tokens.

4. Mocks & seams in this phase

None. This phase is pure client theming — no service seams, no mock flags. The chain's REQ posture: if a backend gap surfaces (none is expected here), append a REQ entry to ../../shared-working-context/frontend/requests/for-backend.md — REQ-001…038 are taken (verified: the tracker ends at REQ-038); number onward from REQ-039. UI stays mock-tolerant behind the existing services/{domain} seams; never edit server/.

5. Critical rules you must not get wrong

  • The two-layer token sync is law — every color added/changed lands in tokens.css (both scheme blocks) and colors.ts in the same commit — and zero hex regressions: feature code has effectively no hard-coded hexes; the theme pass must not reintroduce literals outside the theme layer (deleting PencilIcon removes the last offender).
  • Do not break the RTL dual-theme/Emotion setup (APP_THEME_LTR/APP_THEME_RTL stay module-load-built; createTheme() never in a component; overrides use logical properties only) or the cookie-SSR color-scheme sync (colorSchemeSelector stays the explicit 'data-mui-color-scheme', never 'data'; no InitColorSchemeScript; no storageWindow).
  • The AppIcon registry indirection stays — keep the <AppIcon icon="name"> API, the snake_case domain names, the lowercase keys. AppButton's composition API stays (to/href → AppLink, icon-name startIcon/endIcon) — 238 call-sites; the margin removal is a default change, not an API change.
  • Keep the keep-lists. Notably: textTransform: 'none', radius 10 as the house radius, the lifted dark-teal palette, per-locale font loading (Mikhak never ships to /en; mirror that discipline if you wire Space Grotesk), and NotistackProvider staying token-driven.
  • Design-contract non-negotiables: any new user-facing string (e.g. the logo's aria/alt) goes in both messages/en.json and messages/fa.json; MUI v9 API only; co-located *.test.tsx updated for every touched shared component (AppIcon, AppButton, BrandMark, …); fetch/cookies rules untouched.
  • Update the skill and client/CLAUDE.md in the same change (3.10 + §8) — stale design docs are how the next phase reintroduces the starter.

6. Definition of Done

On top of the shared definition-of-done.md:

  • npm run check green; npm run test:ci green incl. updated tests for every touched shared component; en.json/fa.json in sync.
  • theme.ts has a components block covering at least the 3.1 list; no override uses a raw hex or a physical direction property.
  • LIGHT_PALETTE/DARK_PALETTE define success/error/warning/info; an inline <Alert severity="success"> and a success toast are visibly the same color family in both schemes.
  • TYPOGRAPHY_RTL has letterSpacing: 0 everywhere + the new line-heights and responsive heading sizes; no fontWeight: 600 remains in src (or a real 600 face is loaded — per the documented decision); the Space Grotesk question is resolved (wired or deleted).
  • tokens.css (both blocks) carries elevation, radius, motion, focus-ring, rating, money-emphasis, and trust tokens, mirrored in colors.ts where palette-level.
  • ICONS.logo is the new Balinyaar mark; PencilIcon.tsx, light.ts, dark.ts, AppImage, and the eight dead icon entries are deleted; favicon + webmanifest rebuilt with brand colors and only real files referenced.
  • <AppIcon icon="verified" size={48} /> actually renders 48px; the registry is one visual family; back/chevron_start mirror correctly under dir="rtl"; AppButton has no default margin and zero sx={{ m: 0 }} neutralizations remain in src.
  • First visit with OS dark preference and no cookie paints dark with no light flash; browser theme-color matches the scheme in both modes.
  • Visual verification on the four axes — /fa + /en × light + dark — and mobile + desktop widths on the §7 walk, with screenshots in the report; skill + client/CLAUDE.md updated to match reality.

7. How to test (what a human can verify after this phase)

  1. Open /fa/login (private window, OS set to dark, no cookies): the page paints dark immediately — no light flash — and shows the new Balinyaar logomark (not a pencil), recoloring correctly in dark mode.
  2. On the customer home /fa: buttons are flat (no elevation) with comfortable padding; cards show hairline borders + soft teal-tinted shadows (not grey); chips are soft-tinted; nothing renders stock-MUI grey/blue. Tab through: every focusable element shows the same 2px brand focus ring.
  3. Visit /fa/nurse and /fa/admin (seeded accounts): the top bar is a cream/paper surface with a hairline divider — not a solid teal slab; sidebar icons are one visual family (all Rounded); the selected nav item uses the soft-primary fill.
  4. Trigger an inline alert (e.g. a form error state) and a toast on the same screen: both use the same brand-harmonized semantic colors, in light and dark.
  5. On /fa, inspect Persian text: no letter-spacing gaps inside joined words, headings don't clip ascenders, bold shows a real weight hierarchy (not everything Bold). Compare /en: headings render per the 3.3 decision (Space Grotesk or the documented system stack).
  6. Check the browser tab: the favicon is the new mark; toggling OS dark mode flips the browser chrome (theme-color) to the deep-teal value.

8. Hand off & document (close the phase)

  • Update client/CLAUDE.md: the Theme System section (ColorSchemeScript now real — remove the doc-drift note; new tokens; the weight decision), the theme entries in "Project Structure" (light.ts/dark.ts removed, new icon assets), and the Fonts table if Space Grotesk was wired. Update .claude/skills/frontend-designer/SKILL.md per 3.10 (part of the phase, not optional).
  • Write the frontend report at dev/shared-working-context/reports/ui-phase-0-report.md: what changed at the theme layer, the type-scale + weight + icon-family decisions, before/after screenshots on the four axes, what later phases must know (new token names, directional-icon rule), and any REQs filed (expected: none).
  • Save a memory note per operating-rules §8: the theme/token/icon/brand decisions, the size-bug fix, the margin-default removal, and the no-flash boot mechanism — later UI phases build on all of them.