Initial commit: nick docs
This commit is contained in:
250
05 - Design System/Theme Configuration.md
Normal file
250
05 - Design System/Theme Configuration.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
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: { 100…900 },
|
||||
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]]
|
||||
Reference in New Issue
Block a user