Files
nick-doc/05 - Design System/Theme Configuration.md
Siavash Sameni dceaf82934 audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs:
- Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md
- 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer).
- Scanner docs from scratch (was zero): architecture, data model, API ref, payment
  flow, operations runbook + repo README.
- Doc-sync updates across API reference, data models, flows, design system.
- Secret Rotation Runbook (08 - Operations) for the exposed credentials.
- Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js.

Issues remain status:open intentionally — the code fixes are uncommitted-then-committed
working-tree changes per repo and aren't "resolved" until merged/deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:48:04 +04:00

8.1 KiB
Raw Blame History

title, tags, created, updated
title tags created updated
Theme Configuration
design-system
theme
mui
2026-05-23 2026-05-30

Theme Configuration

[!info] Amaneh v2.7.0 (commit 56fc84e) The active theme now uses the Amaneh warm-earth palette and the three-font stack (Source Serif 4 / IBM Plex Sans / IBM Plex Mono). MUI component overrides were updated for Button, Card, Paper, AppBar, Chip, and Label. The settings-drawer color-preset swatch picker was simplified to a single Amaneh entry.

The MUI theme is constructed in frontend/src/theme/index.ts and composed from option modules in frontend/src/theme/options/. The resulting theme is provided at the root layout, wrapped by an RTL-aware emotion cache.


1. Construction pipeline

// approximate — read theme/index.ts for the canonical version
import { createTheme } from '@mui/material/styles';
import { palette } from './options/palette';
import { typography } from './options/typography';
import { shadows, customShadows } from './options/shadows';
import { componentsOverrides } from './options/overrides';

export function buildTheme(opts: { mode: 'light' | 'dark'; direction: 'ltr' | 'rtl'; preset: string; }) {
  return createTheme({
    direction: opts.direction,
    palette: palette(opts.mode, opts.preset),
    typography,
    shape: { borderRadius: 8 },
    shadows: shadows(opts.mode),
    customShadows: customShadows(opts.mode),
    components: componentsOverrides(opts),
    breakpoints: {
      values: { xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 },
    },
  });
}

The settings context calls buildTheme() whenever any axis changes, then re-mounts <ThemeProvider>. Because MUI v7 supports CSS variables, the swap is cheap (no React tree thrash).


2. Provider wiring (root layout)

// frontend/src/app/layout.tsx (simplified)
<AppRouterCacheProvider options={{ key: dir === 'rtl' ? 'css-rtl' : 'css', prepend: true, stylisPlugins: dir === 'rtl' ? [rtlPlugin] : [] }}>
  <ThemeProvider theme={theme}>
    <CssBaseline />
    <LocalizationProvider dateAdapter={AdapterDayjs}>
      <I18nProvider>
        <QueryClientProvider client={queryClient}>
          <SocketProvider>
            <SnackbarProvider>{children}</SnackbarProvider>
          </SocketProvider>
        </QueryClientProvider>
      </I18nProvider>
    </LocalizationProvider>
  </ThemeProvider>
</AppRouterCacheProvider>

Warning

The cache key MUST differ between LTR and RTL ('css' vs 'css-rtl') — otherwise switching direction at runtime keeps the previous direction's CSS in the head and the layout breaks.


3. Palette

See Colors for the full table. Structure:

{
  mode: 'light' | 'dark',
  primary:   { lighter, light, main, dark, darker, contrastText },
  secondary: { lighter, light, main, dark, darker, contrastText },
  info:      { lighter, light, main, dark, darker, contrastText },
  success:   { lighter, light, main, dark, darker, contrastText },
  warning:   { lighter, light, main, dark, darker, contrastText },
  error:     { lighter, light, main, dark, darker, contrastText },
  grey:      { 100900 },
  text:      { primary, secondary, disabled },
  background:{ default, paper, neutral },
  action:    { active, hover, selected, focus, disabled, disabledBackground },
}

Color presets (selectable in settings) swap the primary + secondary color groups while leaving grey, error, etc. untouched.


4. Typography

{
  fontFamily: '"Public Sans Variable", "Helvetica", "Arial", sans-serif',
  fontWeightRegular: 400,
  fontWeightMedium: 500,
  fontWeightSemiBold: 600,
  fontWeightBold: 700,
  h1: { fontSize: 64, lineHeight: 80/64, letterSpacing: -1 },
  h2: { fontSize: 48, lineHeight: 64/48 },
  h3: { fontSize: 32, lineHeight: 48/32 },
  h4: { fontSize: 24, lineHeight: 36/24 },
  h5: { fontSize: 20, lineHeight: 30/20 },
  h6: { fontSize: 18, lineHeight: 28/18 },
  subtitle1: { fontSize: 16, fontWeight: 600 },
  subtitle2: { fontSize: 14, fontWeight: 600 },
  body1: { fontSize: 16 },
  body2: { fontSize: 14 },
  caption: { fontSize: 12 },
  overline: { fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1.1 },
  button: { fontSize: 14, fontWeight: 700, textTransform: 'unset' },
}

Variants h1 and h2 scale down responsively. Use the responsiveFontSizes(theme) helper if you need automatic scaling.

See Typography for font loading, secondary font (Barlow), and i18n font fallbacks.


5. Spacing

spacing: 8  // unit
// theme.spacing(1) === '8px'
// theme.spacing(2) === '16px'
// theme.spacing(0.5) === '4px'

Use the shorthand props on the sx:

<Box sx={{ p: 2, mt: 3, mx: { xs: 1, md: 4 } }} />

6. Shape & radii

shape: { borderRadius: 8 }   // default
// cards typically: borderRadius: 16
// pills:         borderRadius: 9999

7. Shadows

MUI's default shadows array (25 levels: none, then 124) is replaced with a softer custom set. Additionally, customShadows defines brand-specific tinted shadows used by the Card, Button, Dialog overrides:

customShadows: {
  z1, z4, z8, z12, z16, z20, z24,
  primary:   'rgba(<primary.main>, 0.24) 0 8 16 0',
  secondary: '…',
  info:      '…',
  success:   '…',
  warning:   '…',
  error:     '…',
  card:      'rgba(0,0,0,0.04) 0 0 2 0, rgba(0,0,0,0.08) 0 12 24 -4',
  dialog:    '…',
  dropdown:  '…',
}

Read inside sx:

sx={{ boxShadow: (theme) => theme.customShadows.z8 }}

8. Component overrides

theme/options/overrides/ contains one file per overridden component, each exporting { defaultProps, styleOverrides } merged into components:

Component Notable override
MuiButton disableRipple, custom customShadows.primary on contained variant
MuiCard borderRadius: 16, customShadows.card
MuiPaper elevation flattened to 0 by default (use card shadow explicitly)
MuiTextField variant: 'outlined' default, custom focus ring
MuiTooltip dark background regardless of mode, smaller padding
MuiAvatar letter-based fallback with hashed background color
MuiTableCell denser padding, header weight 600
MuiDialog customShadows.dialog, max-width responsive
MuiAlert tinted background using palette lighter
MuiTab uppercase off, color from primary.main when selected
MuiAutocomplete custom popup paper shadow

Add an override:

  1. Create theme/options/overrides/<Component>.ts exporting a function (theme) => ({...}).
  2. Register it in theme/options/overrides/index.ts.
  3. It will be merged into the next createTheme call automatically.

9. CSS variables (MUI v7)

createTheme with cssVariables: true (recommended) emits CSS custom properties at the <html> root and switches them per mode without re-rendering. The default is on in v7 — verify in theme/index.ts.

This means a custom CSS rule can reference:

.my-thing {
  background: var(--mui-palette-primary-main);
  padding: calc(var(--mui-spacing) * 2);
}

Use this sparingly — prefer the sx prop.


10. Per-locale font swap

When the active locale is fa or ar, the typography option module can layer a Persian/Arabic font (e.g., Vazirmatn, IRANSans) ahead of Public Sans Variable:

fontFamily: dir === 'rtl'
  ? '"Vazirmatn", "Public Sans Variable", sans-serif'
  : '"Public Sans Variable", sans-serif',

Otherwise Latin glyphs still render via the Public Sans fallback. See Internationalization & RTL.


11. Customisation checklist

  • New brand color → update palette.ts (light + dark) and consider a preset.
  • New variant → add to typography options and the TypographyOptions interface augmentation.
  • Component override → file in overrides/.
  • Verify in both modes (light + dark) AND both directions (ltr + rtl).
  • Verify against WCAG AA contrast.
  • Snapshot test the component if visual stability matters.