Files
baya-monorepo/archive/build-chain/working-context/reports/ui-phase-0-report.md
T
2026-08-02 18:48:32 +03:30

14 KiB
Raw Blame History

UI Phase 0 — Design language & theme foundation — Report (2026-07-17)

What was built

theme.components pass (the highest-leverage change)

client/src/theme/theme.ts now carries a components block covering MuiCssBaseline (global :focus-visible → 2px var(--bal-focus-ring)), MuiButton (disableElevation, --bal-radius-sm, comfortable padding), MuiPaper (hairline var(--bal-divider) border, backgroundImage: none), MuiAppBar (cream/paper surface + hairline bottom border, color="transparent" default — no more solid teal slab), MuiOutlinedInput (house radius + calmer resting border + primary hover), MuiChip (soft --bal-primary-soft fill for default-color filled chips only — ownerState-scoped so color="success"/"error" chips are untouched), MuiToggleButton/MuiListItemButton (.Mui-selected → primary-soft fill + primary text), MuiDialog (--bal-radius-lg, tighter mobile margins), MuiTabs/MuiTab (3px indicator, 48px min-height), MuiAlert (house radius — severities now come from the palette, see below), MuiStepIcon (brand active/completed), MuiSkeleton (warm primary-soft tint), MuiTooltip (ink/cream inversion). Every value is var(--bal-*) or a logical CSS property — zero raw hex, zero physical direction props.

A theme.shadows array (TEAL_SHADOWS, 25 entries tiered onto --bal-shadow-1/2/3) replaces MUI's default grey stack globally — every component that reads theme.shadows[n] (Dialog, Menu, Popover, AppBar elevation, Autocomplete…) gets teal-tinted shadows for free, not just the ones this file overrides directly. Both themes are wrapped in responsiveFontSizes() for per-breakpoint heading scaling.

Semantic palette

LIGHT_PALETTE/DARK_PALETTE (colors.ts) now define success/error/warning/info (main + contrastText), sourced from the same values as the --bal-* feedback tokens — an inline <Alert severity="success"> and a success toast are now the same color family in both schemes. AppAlert's defaults flipped from the footgun (severity="error" variant="filled") to a calm baseline (severity="info" variant="standard") — every existing call-site already passed an explicit severity+variant, so this was a safe default change with zero call-site migration needed (verified by grep before changing).

Persian type scale

typography.ts rewritten: letterSpacing: 0 on every TYPOGRAPHY_RTL variant, a shared size/line-height scale (SIZE_SCALE — body line-height 1.7, headings 1.41.5, no more 6rem default h1), and the weight decision: 700 for headings/buttons, 500 for in-text emphasis, 400 body — never 600 (neither Mikhak nor Space Grotesk loads a 600 face; a requested 600 silently rendered full Bold). Swept 64 fontWeight: 600 sx call-sites across 42 files to 500/700 by this rule (9→700, 55→500; judgment calls documented in the commit — e.g. selection-card titles → 700, chip/row labels → 500). LTR keeps its original split (Space Grotesk headings, system-font body); RTL keeps Mikhak everywhere (full glyph coverage). Space Grotesk is now genuinely wired via next/font/google in app/[locale]/layout.tsx (self-hosted at build time, preload: false, attached only on en — mirrors the Mikhak/fa discipline exactly); the dead "not currently wired" comment is gone.

Token extension (tokens.css + colors.ts mirror where palette-level)

Added, both scheme blocks: --bal-radius-sm/md/lg (4/10/16 — controls/cards/dialogs), --bal-motion-fast/base/slow + --bal-easing-standard (for phase 12), --bal-shadow-1/2/3 (teal-tinted light, black-teal dark), --bal-focus-ring, --bal-rating/--bal-rating-empty (retiring the muddy --bal-warning star fill — phase 1 wires this into RatingInput), --bal-trust/--bal-trust-soft (a distinct blue identity for verified marks, separate from primary/success — phases 1/4/8 consume this), --bal-money-emphasis (AA-contrast-safe; --bal-secondary fails AA at small sizes on white, ~2:1). tokens.css's header now points at the frontend-designer skill instead of the deleted product/balinyaar.html.

Brand mark — the pencil is gone

Two new SVGs under AppIcon/icons/: LogoMark.tsx (monochrome currentColor "b" glyph — stem + a true evenodd-ring bowl, no background square — registered as ICONS.logo, used anywhere via <AppIcon icon="logo">, recolors for free in dark mode) and LogoLockup.tsx (the full-color mark: deep-teal rounded-square ground, cream glyph, terracotta dot, all var(--bal-*)-driven) used directly by BrandMark.tsx next to the (still translated, never-baked-into-SVG) wordmark. PencilIcon.tsx (the Twemoji starter pencil, the app's last hard-coded-hex SVG) is deleted. Favicon + webmanifest regenerated from the same construction via a one-off sharp script (fixed brand hex — the one place a literal hex is correct, since it's a static binary asset, not app code): src/app/favicon.ico (real multi-size PNG-in-ICO, 16/32/48) and public/img/favicon/{16,32,48,180,192,512}.png all now render the actual mark; site.webmanifest's theme_color/background_color are the real brand teal/cream (were starter #000000/#ffffff), and every referenced icon file now genuinely exists.

Icon system

  • Size bug fixed: AppIcon.tsx drives size via style.fontSize (which MUI SvgIcon's 1em-based CSS actually respects) instead of width/height attributes (which that same CSS silently beat) — all ~40 existing size={14..56} call-sites now render at the requested size. The invalid size DOM attribute is no longer spread onto the SVG; the unknown-icon console.warn is dev-only.
  • One visual family: the ~90-entry registry (AppIcon/config.ts) is now 100% MUI *Rounded — every filled/outlined starter icon was swapped. The 8 dead starter entries (daynight/night/day/visibilityon/visibilityoff/signup/login/settings) are deleted (re-verified zero usages before deleting).
  • New vocabulary: back/chevron_start, share, copy, phone, navigate, sort, attachment, star_half.
  • Directional mirroring: back/chevron_start are registered in a new DIRECTIONAL_ICONS set; AppIcon stamps data-icon-directional on those, and one CSS rule (app/globals.css) does the flip ([dir='rtl'] [data-icon-directional] { transform: scaleX(-1) }) — adding a future directional icon is a one-line registry addition, never a per-component flip.

AppButton de-startering

Removed DEFAULT_SX_VALUES = { margin: 1 } and the dead underline/label/text prop cruft (text/label duplicate props stay — 238 call-sites use them — only the invalid underline spread onto non-link buttons and the false "Box around to specify margins" JSDoc were removed). Swept the now-redundant sx={{ m: 0 }} neutralization at 192 call sites across ~68 files (6 parallel batches, verified against the original 213-occurrence/82-file audit count — the ~21 difference is re-count timing, not missed sites); 3 genuine non-AppButton m: 0 uses (on FormControlLabel, an unrelated real layout decision) were correctly left alone. Outer spacing is now the parent's job (Stack/Box gaps), as intended.

Starter residue purge

Deleted theme/light.ts + theme/dark.ts (+ their theme/index.ts exports: APP_THEME/LIGHT_THEME/DARK_THEME/LIGHT_THEME as default — nothing imported them) and the deprecated TYPOGRAPHY alias. Deleted the unused AppImage component + its test + barrel exports (zero product usages, verified). Rewrote globals.css as an intentional minimal base: kept the box-sizing reset, dropped max-height: 100vh and the max-width: 100vw; overflow-x: hidden mask, added ::selection in brand colors + the directional-icon CSS rule, and a commented decision on the a { color: inherit } reset. Fixed the false CONTENT_MIN_WIDTH comment and the starter commented-alternatives style in components/config.ts.

No-flash color-scheme boot — CSS only, no boot script

The original phase spec called for an inline pre-paint <script> (a ColorSchemeScript, matching what client/CLAUDE.md's Theme System section had — inaccurately — already documented). Built, then deliberately reverted per explicit user direction mid-session: a hand-rolled script (cookie parsing + Storage.prototype patching) didn't fit a codebase where every other theme decision is CSS-variable-driven. The shipped mechanism is pure CSS:

  • Returning visitor (cookie present): unchanged — server stamps data-mui-color-scheme, the explicit tokens.css blocks match immediately.
  • First-ever visitor (no cookie): getThemeMode() now returns colorScheme: undefined, so the server renders <html> without the attribute at all. tokens.css gained a @media (prefers-color-scheme: dark) block scoped to :root:not([data-mui-color-scheme]) that paints the OS-preferred scheme immediately, zero JS. Once React hydrates, defaultMode="system" resolves the same media query and MUI stamps the attribute itself — the CSS values already match, so nothing visibly flips.
  • Known trade-off (documented in client/CLAUDE.md): this covers every --bal-* token (the dominant visual surface — page/paper background, text, dividers, all brand colors). MUI's own --mui-palette-* variables don't get the same free fallback (MUI's colorSchemeSelector supports attribute-based or 'media'-based generation, not both), so a bare color="primary" fill (e.g. a contained Button) can very briefly show the light value on a cookie-less dark-OS first visit before hydration — self-corrects same frame, never animates (disableTransitionOnChange). Acceptable trade-off given the alternative was a hand-rolled script the codebase's own conventions argue against.

viewport.themeColor is now the media-query array form (light → BRAND.teal, dark → BRAND.tealDeep) so browser chrome matches the page in both schemes.

What is now testable (and exactly how)

  1. npm run dev, open /fa/login in a private window with OS dark on and no cookies: dark paint from first frame (no light flash on the --bal-* surface), the new Balinyaar mark (not a pencil) on the auth splash, recoloring correctly if you toggle dark/light.
  2. /fa customer home: flat buttons with comfortable padding, hairline-bordered cards with soft teal shadows (not grey), soft-tinted chips. Tab through the page — every focusable element shows the same 2px brand focus ring.
  3. /fa/nurse and /fa/admin (seeded accounts): top bar is a cream/paper surface with a hairline divider, not a solid teal slab; sidebar icons are one Rounded family; selected nav item uses the soft-primary fill.
  4. Trigger an inline form-error AppAlert and a toast on the same screen — same brand-harmonized color family, both schemes.
  5. /fa Persian text: no letter-spacing gaps inside joined words, headings don't clip; compare /en — Space Grotesk headings render (network tab shows the self-hosted font file only on /en, never on /fa).
  6. Browser tab: favicon is the new mark; toggling OS dark mode flips theme-color (deep-teal) in supporting browsers.
  7. npm run check (type + lint) and npm run test:ci — both green (see Gate below).

What is mocked / waiting on a real service

None — this phase is pure client theming, no service seams, no mock flags (per the phase's own §4).

Docs updated

  • .claude/skills/frontend-designer/SKILL.md — brand-mark construction (now the source of truth, product/balinyaar.html is gone), the new token categories + when to use each, the weight-500/700 decision (the old "buttons weight 600" line was wrong the moment this phase landed), the Rounded icon family + directional-mirroring rule + the size-bug fix, dropped the stale ~19-icon "currently registered" list (pointed at AppIcon/config.ts directly instead — it drifts too fast for a skill doc to enumerate reliably).
  • client/CLAUDE.md — Theme System section rewritten for the CSS-only no-flash mechanism (replacing the doc-drift ColorSchemeScript description that never matched reality), the Fonts table (Space Grotesk now wired), the theme/ entries in Project Structure (light.ts/dark.ts removed).

Follow-ups for later phases

  • Phase 1: wire --bal-rating/--bal-rating-empty into RatingInput (currently still uses --bal-warning), build EmptyState/ErrorState/PageHeader/<Money>/Jalali picker/skeleton twins/route-level loading.tsx/error.tsx/404 — all deferred here per the phase's own scope boundary.
  • Phase 2: TopBar/SideBar chrome, UserInfo, nav grouping, SSR mobile-first flash — the physical paddingLeft/paddingRight in TopBarAndSideBarLayout.tsx and the hard-coded English "Open Sidebar" tooltip are pre-existing shell issues, deliberately untouched here (no page/shell redesigns in phase 0).
  • Phase 4/8: --bal-trust/--bal-trust-soft are defined but not yet consumed by TrustBadge/verification UI — that's a content decision for those phases, not a token gap.
  • Phase 12: the app-wide motion pass consumes --bal-motion-*/--bal-easing-standard, defined but unused here (define-only per this phase's scope).
  • No REQs filed — this phase never touches server/ or any contract.

Gate

  • npm run check (type + lint): green, zero errors/warnings.
  • npm run test:ci: 329/329 tests, 79/79 suites green. Three pre-existing shared-component tests (AppIcon.test.tsx, AppIconButton.test.tsx, AppButton.test.tsx) asserted the exact bugs this phase fixes (an invalid size DOM attribute; the pre-Rounded-swap MoreHorizIcon testid) — updated in the same change to assert the corrected behavior (style.fontSize, MoreHorizRoundedIcon) rather than the old bug.
  • en.json/fa.json: untouched — this phase added no new user-facing strings (the brand mark's alt text reuses the existing common.brand key via BrandMark's adjacent <Typography>; the SVG itself is aria-hidden).