Files
nick-doc/05 - Design System/Theme Configuration.md
2026-05-23 20:35:34 +03:30

251 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Theme Configuration
tags: [design-system, theme, mui]
created: 2026-05-23
---
# Theme Configuration
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
```ts
// 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)
```tsx
// 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:
```ts
{
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
```ts
{
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
```ts
spacing: 8 // unit
// theme.spacing(1) === '8px'
// theme.spacing(2) === '16px'
// theme.spacing(0.5) === '4px'
```
Use the shorthand props on the `sx`:
```tsx
<Box sx={{ p: 2, mt: 3, mx: { xs: 1, md: 4 } }} />
```
---
## 6. Shape & radii
```ts
shape: { borderRadius: 8 } // default
// cards typically: borderRadius: 16
// pills: borderRadius: 9999
```
---
## 7. Shadows
MUI's default `shadows` array (25 levels: `none`, then `1``24`) is replaced with a softer custom set. Additionally, `customShadows` defines brand-specific tinted shadows used by the `Card`, `Button`, `Dialog` overrides:
```ts
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`:
```tsx
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:
```css
.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`:
```ts
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.
---
## Related
- [[Design System Overview]]
- [[Colors]] · [[Typography]] · [[Components]] · [[Layouts]]
- [[Internationalization & RTL]] · [[Settings & Theming]]
- [[Frontend Architecture]]