Initial commit: nick docs
This commit is contained in:
207
05 - Design System/Colors.md
Normal file
207
05 - Design System/Colors.md
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
title: Colors
|
||||
tags: [design-system, colors, palette]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Colors
|
||||
|
||||
The palette is built from semantic groups (`primary`, `secondary`, `info`, `success`, `warning`, `error`, plus a 9-step `grey` scale) and exposed via the MUI theme. **Never hard-code hex values in components.**
|
||||
|
||||
> [!warning]
|
||||
> Hardcoded colors break dark mode and any future preset switch. Use `sx={{ color: 'primary.main' }}` or `theme.palette.primary.main`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Semantic groups
|
||||
|
||||
Every color group has the shape:
|
||||
|
||||
```ts
|
||||
{
|
||||
lighter: '#…', // tint background, alerts
|
||||
light: '#…', // hover surfaces, badges
|
||||
main: '#…', // primary use (buttons, links)
|
||||
dark: '#…', // hover state of `main`
|
||||
darker: '#…', // pressed state
|
||||
contrastText: '#…', // WCAG AA on `main`
|
||||
}
|
||||
```
|
||||
|
||||
### 1.1 Primary
|
||||
|
||||
| Token | Light hex (default preset) | Dark hex |
|
||||
|---|---|---|
|
||||
| `primary.lighter` | `#D0ECFE` | `#CCF4FE` |
|
||||
| `primary.light` | `#73BAFB` | `#68CDF9` |
|
||||
| `primary.main` | `#1877F2` | `#078DEE` |
|
||||
| `primary.dark` | `#0C44AE` | `#0351AB` |
|
||||
| `primary.darker` | `#042174` | `#012972` |
|
||||
| `primary.contrastText` | `#FFFFFF` | `#FFFFFF` |
|
||||
|
||||
Used for primary actions, links, focused inputs.
|
||||
|
||||
### 1.2 Secondary
|
||||
|
||||
Typical: a complementary purple/teal. Confirm by reading `theme/options/palette.ts`.
|
||||
|
||||
### 1.3 Info / Success / Warning / Error
|
||||
|
||||
Standard semantic colors:
|
||||
|
||||
| Group | Hue | Used for |
|
||||
|---|---|---|
|
||||
| `info` | cyan/blue | Neutral informative banners |
|
||||
| `success` | green | Confirmed payments, accepted offers |
|
||||
| `warning` | amber | Pending state, mild alerts |
|
||||
| `error` | red | Disputes, validation errors, destructive |
|
||||
|
||||
Each follows the same `lighter/light/main/dark/darker/contrastText` shape.
|
||||
|
||||
### 1.4 Grey
|
||||
|
||||
Nine-step scale (100 → 900). Use for:
|
||||
|
||||
- `grey.100`–`grey.300` → backgrounds, dividers, disabled
|
||||
- `grey.500` → secondary text, muted icons
|
||||
- `grey.700`–`grey.900` → primary text, dark headings
|
||||
|
||||
### 1.5 Text & Background
|
||||
|
||||
```ts
|
||||
text: {
|
||||
primary: grey[800], // body
|
||||
secondary: grey[600], // muted
|
||||
disabled: grey[500],
|
||||
},
|
||||
background: {
|
||||
default: '#FFFFFF', // light mode page
|
||||
paper: '#FFFFFF', // cards, dialogs
|
||||
neutral: grey[200], // page wash
|
||||
},
|
||||
```
|
||||
|
||||
In dark mode these invert (`default` becomes near-black, `paper` slightly lighter, `text.primary` near-white).
|
||||
|
||||
---
|
||||
|
||||
## 2. Action states
|
||||
|
||||
```ts
|
||||
action: {
|
||||
active: alpha(grey[600], 1),
|
||||
hover: alpha(grey[500], 0.08),
|
||||
selected: alpha(grey[500], 0.16),
|
||||
disabled: alpha(grey[500], 0.8),
|
||||
disabledBackground: alpha(grey[500], 0.24),
|
||||
focus: alpha(grey[500], 0.24),
|
||||
hoverOpacity: 0.08,
|
||||
disabledOpacity: 0.48,
|
||||
}
|
||||
```
|
||||
|
||||
Use via `sx={{ bgcolor: 'action.hover' }}` etc.
|
||||
|
||||
---
|
||||
|
||||
## 3. Color presets
|
||||
|
||||
The settings drawer ships several **color presets** that swap only `primary` (and optionally `secondary`) while keeping every other group stable. Typical presets (confirm in `theme/options/presets/`):
|
||||
|
||||
| Preset | Primary hue | Vibe |
|
||||
|---|---|---|
|
||||
| `default` | Blue | Trust, finance |
|
||||
| `purple` | Indigo/Purple | Premium |
|
||||
| `cyan` | Cyan | Tech, fresh |
|
||||
| `blue` | Vibrant blue | Strong CTA |
|
||||
| `orange` | Orange | Energetic |
|
||||
| `red` | Brand red | Bold |
|
||||
|
||||
To add a preset:
|
||||
|
||||
1. Add a `<name>.ts` file in `theme/options/presets/` exporting `{ lighter, light, main, dark, darker, contrastText }`.
|
||||
2. Register in the presets index map.
|
||||
3. Surface in the settings drawer color picker (`src/settings/drawer/`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Contrast verification
|
||||
|
||||
WCAG AA targets:
|
||||
|
||||
| Element | Min contrast |
|
||||
|---|---|
|
||||
| Body text (≥18 px regular or ≥14 px bold) | **4.5 : 1** |
|
||||
| Large text (≥24 px regular or ≥18 px bold) | **3 : 1** |
|
||||
| Icon-only buttons | **3 : 1** for their boundary |
|
||||
| Form input borders | **3 : 1** |
|
||||
|
||||
> [!tip]
|
||||
> When picking a new `main` color, run it through [Stark](https://stark.co/) or the Chrome DevTools "Inspect color" checker against `background.paper` AND `background.default` for both light and dark modes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Using colors in `sx`
|
||||
|
||||
```tsx
|
||||
// Token-based — preferred
|
||||
<Box sx={{ bgcolor: 'primary.main', color: 'primary.contrastText' }} />
|
||||
|
||||
// Function form when you need to derive
|
||||
<Box sx={(theme) => ({ bgcolor: alpha(theme.palette.primary.main, 0.08) })} />
|
||||
|
||||
// In styled component
|
||||
const Item = styled('div')(({ theme }) => ({
|
||||
background: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Charts & data viz
|
||||
|
||||
Use the helper `theme.palette.<group>.main` array for series colors. The `chart/` component family (ApexCharts wrapper) reads these tokens so the chart palette stays in sync with the theme.
|
||||
|
||||
For categorical data with >7 series, layer in `secondary.main`, `info.main`, `success.main`, `warning.main`, `error.main`, then darker variants — never invent ad-hoc hex.
|
||||
|
||||
---
|
||||
|
||||
## 7. Diff visualisation (chat, evidence)
|
||||
|
||||
| Concept | Color token |
|
||||
|---|---|
|
||||
| Added / accepted | `success.main` with `success.lighter` background |
|
||||
| Removed / rejected | `error.main` with `error.lighter` background |
|
||||
| Pending review | `warning.main` |
|
||||
| Informational | `info.main` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Avatar backgrounds
|
||||
|
||||
`MuiAvatar` override hashes the user's name to one of the semantic `main` colors so the same person always gets the same background. Implementation in `components/user-avatar/`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Brand vs semantic
|
||||
|
||||
| Use case | Token |
|
||||
|---|---|
|
||||
| Primary CTA button | `primary.main` |
|
||||
| Success snackbar | `success.main` |
|
||||
| Error border on form field | `error.main` |
|
||||
| Page background | `background.default` |
|
||||
| Card background | `background.paper` |
|
||||
| Sidebar background | `background.neutral` |
|
||||
| Divider line | `divider` |
|
||||
| Primary text | `text.primary` |
|
||||
| Muted helper text | `text.secondary` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Related
|
||||
|
||||
- [[Theme Configuration]] · [[Design System Overview]] · [[Typography]]
|
||||
- [[Components]] — components consuming these tokens
|
||||
- [[Settings & Theming]] — preset switcher
|
||||
222
05 - Design System/Components.md
Normal file
222
05 - Design System/Components.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
title: Components
|
||||
tags: [design-system, components, ui]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Components
|
||||
|
||||
Inventory of reusable components in `frontend/src/components/`. Each component lives in its own folder (`index.ts` + `<name>.tsx` + `classes.ts` + `types.ts`).
|
||||
|
||||
> [!info]
|
||||
> Pattern: components are reusable **and free of business logic**. Page-specific composition lives in `frontend/src/sections/`. Hooks that fetch data live in `frontend/src/hooks/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Form components (`components/hook-form/`)
|
||||
|
||||
RHF + MUI wrappers. Used inside a `<FormProvider {...methods}>` context. All accept `name`, plus the MUI props of the underlying component.
|
||||
|
||||
| Component | Wraps | Notes |
|
||||
|---|---|---|
|
||||
| `RHFTextField` | `TextField` | The workhorse text input |
|
||||
| `RHFSelect` | `Select` + `MenuItem` | Pass `children={<MenuItem value="..."/>...}` |
|
||||
| `RHFAutocomplete` | `Autocomplete` | Async-loader friendly; pass `options` |
|
||||
| `RHFCheckbox` | `Checkbox` + `FormControlLabel` | Single boolean |
|
||||
| `RHFMultiCheckbox` | grid of checkboxes | Returns `string[]` |
|
||||
| `RHFRadioGroup` | `RadioGroup` | Returns selected value |
|
||||
| `RHFSwitch` | `Switch` + label | Boolean toggle |
|
||||
| `RHFEditor` | TipTap wrapper | HTML output |
|
||||
| `RHFUpload` | `components/upload` Dropzone | Single or multiple |
|
||||
| `RHFDatePicker` | `@mui/x-date-pickers DatePicker` | Locale-aware |
|
||||
| `RHFDateTimePicker` | `DateTimePicker` | — |
|
||||
| `RHFTimePicker` | `TimePicker` | — |
|
||||
| `RHFPhoneInput` | `react-phone-number-input` | E.164 output |
|
||||
| `RHFCountrySelect` | `components/country-select` | ISO-2 output |
|
||||
| `RHFNumberInput` | `components/number-input` | Locale-formatted, accepts min/max/step |
|
||||
| `RHFRating` | `Rating` | 0–5 |
|
||||
| `RHFSlider` | `Slider` | Single or range |
|
||||
|
||||
> [!tip]
|
||||
> Form-level errors render automatically via `formState.errors[name]` — pass `helperText={errors.name?.message}` is **not needed**; the wrapper handles it.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data display
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `custom-data-grid/` | MUI DataGrid Pro wrapper — sets density, locale, custom toolbar, RTL support |
|
||||
| `table/` | Lower-level table utilities (head, pagination, no-data row, skeleton) |
|
||||
| `chart/` | ApexCharts wrapper with theme-synced palette |
|
||||
| `markdown/` | `react-markdown` + `remark-gfm` for chat/evidence rendering |
|
||||
| `lightbox/` | Image gallery (gallery / dispute evidence) |
|
||||
| `image/` | Next `Image` wrapper with fallback skeleton + error state |
|
||||
| `file-thumbnail/` | Renders icon by MIME type for non-image files |
|
||||
| `flag-icon/` | Country flag SVG (used in phone input, locale switcher) |
|
||||
| `iconify/` | Iconify SVG wrapper — `<Iconify icon="solar:user-bold" />` |
|
||||
| `svg-color/` | Renders SVG with `color` prop (mask-image trick) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Navigation
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `nav-section/` | Sidebar nav with role-filtering, group headers, mini variant |
|
||||
| `nav-basic/` | Simple horizontal nav for marketing pages |
|
||||
| `custom-breadcrumbs/` | Page header breadcrumbs + action slot |
|
||||
| `progress-bar/` | NProgress integration for route transitions |
|
||||
|
||||
The sidebar reads from a configuration array — see `src/layouts/dashboard/config-nav-dashboard.ts` (or similar) — where each entry can specify `roles: ['admin']` to hide from other users.
|
||||
|
||||
---
|
||||
|
||||
## 4. Feedback
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `snackbar/` | Notistack wrapper — `enqueueSnackbar('Done', { variant: 'success' })` |
|
||||
| `loading-screen/` | Full-screen splash for boot or guarded routes |
|
||||
| `empty-content/` | "No results" placeholder with illustration |
|
||||
| `search-not-found/` | Variant of empty-content for failed searches |
|
||||
| `email-verification-banner/` | Persistent top banner when email unverified |
|
||||
|
||||
---
|
||||
|
||||
## 5. Overlays & dialogs
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `custom-dialog/` | Modal dialog with title/content/actions slots |
|
||||
| `custom-popover/` | Click-or-hover popover (used for menus, tooltips, info) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Inputs (raw, non-RHF)
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `number-input/` | Numeric with +/- buttons + locale format |
|
||||
| `phone-input/` | Phone with country selector |
|
||||
| `country-select/` | Country dropdown |
|
||||
| `incrementer-button/` | Tiny `−` / `+` quantity stepper |
|
||||
| `upload/` | Dropzone with progress, preview, removal |
|
||||
|
||||
---
|
||||
|
||||
## 7. User & profile
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `user-avatar/` | Hashes name → color; respects `src` if avatar uploaded |
|
||||
| `user-profile-card/` | Profile summary card (name, role, level, points) |
|
||||
| `notifications-drawer/` | Side drawer listing notifications, mark-as-read |
|
||||
|
||||
---
|
||||
|
||||
## 8. Media
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `video-player/` | Plyr / video.js wrapper for blog & template videos |
|
||||
| `map/` | Mapbox + react-map-gl wrapper (delivery addresses) |
|
||||
| `carousel/` | Embla carousel — multiple variants in `hooks/use-carousel-*` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Animation
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `animate/` | Framer-motion presets (fade, slide, scale) for page transitions and lists |
|
||||
|
||||
---
|
||||
|
||||
## 10. Utility
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `scrollbar/` | SimpleBar wrapper — themed scrollbar with shadow on overflow |
|
||||
| `label/` | Tag/badge — variants: filled, outlined, soft |
|
||||
| `filters-result/` | Active filter chips with × to remove |
|
||||
| `logo/` | App logo SVG (responsive: full mark + icon-only at small width) |
|
||||
| `color-utils/` | Color manipulation helpers (alpha, darken, contrast) |
|
||||
|
||||
---
|
||||
|
||||
## 11. Settings
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `settings/` | The mounted Settings drawer + provider hooks |
|
||||
|
||||
---
|
||||
|
||||
## 12. Debug
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `debug/` | Dev-only diagnostic overlays (only rendered when `NEXT_PUBLIC_ENABLE_DEBUG=true`) |
|
||||
|
||||
---
|
||||
|
||||
## 13. Editor (`components/editor/`)
|
||||
|
||||
TipTap rich-text editor with this extension set:
|
||||
|
||||
- `StarterKit` (paragraph, headings, bold/italic, lists, blockquote, history)
|
||||
- `Link` (URL parsing, target=_blank by default for external)
|
||||
- `Image` (drag-drop, uploads via `components/upload`)
|
||||
- `Underline`, `TextAlign`
|
||||
- `CodeBlock` + `CodeBlockLowlight` (syntax highlighting via `lowlight`)
|
||||
- `Placeholder`
|
||||
|
||||
Output: HTML string. Round-trip safe with `react-markdown` + `turndown` when Markdown storage is preferred.
|
||||
|
||||
Used in:
|
||||
- Blog post editor (`dashboard/post/new`)
|
||||
- Long-form description fields (request, template)
|
||||
- Dispute evidence narrative
|
||||
|
||||
---
|
||||
|
||||
## 14. Payment-specific (`components/payment/`)
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `<TokenSelector>` | USDT / USDC / BTC / ETH chip selector |
|
||||
| `<NetworkSelector>` | BSC / TRC20 / Ethereum / Polygon chip selector |
|
||||
| `<QrInvoice>` | Renders the SHKeeper invoice QR + copy-address button |
|
||||
| `<PaymentStatus>` | Live status pill that updates from socket events |
|
||||
|
||||
(Confirm names against actual exports — these are typical for this stack.)
|
||||
|
||||
---
|
||||
|
||||
## 15. Adding a new component
|
||||
|
||||
1. Create `src/components/<kebab-name>/` with `index.ts`, `<kebab-name>.tsx`, `classes.ts`, `types.ts`.
|
||||
2. Re-export from `src/components/<kebab-name>/index.ts`: `export * from './<kebab-name>'`.
|
||||
3. Place ONLY rendering + styling; do NOT call `useQuery` / `useMutation` — pass data via props.
|
||||
4. Use the `sx` prop, not styled-components, unless the styling is non-trivial.
|
||||
5. Cover with at least one Jest snapshot test in `__tests__/`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Component vs Section vs Hook decision
|
||||
|
||||
| Question | Where it goes |
|
||||
|---|---|
|
||||
| "I want to render a chip with a delete icon." | `components/` |
|
||||
| "I want a chat sidebar with conversation list + active chat + composer." | `sections/chat/` |
|
||||
| "I want to subscribe to chat updates and keep the cache in sync." | `hooks/use-chat-socket.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 17. Related
|
||||
|
||||
- [[Design System Overview]] · [[Theme Configuration]] · [[Colors]] · [[Typography]]
|
||||
- [[Layouts]] · [[Internationalization & RTL]]
|
||||
- [[Frontend Architecture]] — where these components live in the bigger picture
|
||||
- [[Coding Standards]] — file layout & import order rules
|
||||
179
05 - Design System/Design System Overview.md
Normal file
179
05 - Design System/Design System Overview.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: Design System Overview
|
||||
tags: [design-system, ui, mui]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Design System Overview
|
||||
|
||||
The frontend design system is built on **Material-UI v7** with project-specific tokens, an LTR + RTL-aware emotion cache, and a user-controllable settings drawer (mode, layout, color preset, font, direction).
|
||||
|
||||
> [!info]
|
||||
> All design decisions documented here are enforced by the cursor rules at `backend/.cursor/rules/ui-development-standards.mdc`. When in doubt, that file is the authoritative source.
|
||||
|
||||
---
|
||||
|
||||
## 1. Philosophy
|
||||
|
||||
- **Material first, brand second.** Use MUI components and the `sx` prop with array syntax for responsive overrides. Do NOT subclass primitives unless absolutely necessary.
|
||||
- **Tokens over magic numbers.** All colors, spacings, radii, and shadows come from the theme — never hard-code hex values or pixel offsets.
|
||||
- **RTL-first.** Persian and Arabic are first-class. Layouts must mirror correctly without manual `direction` overrides.
|
||||
- **Accessibility-by-default.** WCAG AA contrast (4.5:1), semantic HTML, keyboard navigation, ARIA labels on icon-only buttons.
|
||||
- **One component, one folder.** Each component lives in `components/<name>/` with `index.ts` + `component.tsx` + `classes.ts` + `types.ts` so it can be refactored without grep-and-replace pain.
|
||||
- **Compose, don't inherit.** Sections compose components, components compose primitives, primitives wrap MUI.
|
||||
|
||||
---
|
||||
|
||||
## 2. Component layering
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
P[Pages — src/app/]
|
||||
S[Sections — src/sections/]
|
||||
C[Components — src/components/]
|
||||
L[Layouts — src/layouts/]
|
||||
H[Hooks — src/hooks/]
|
||||
T[Theme — src/theme/]
|
||||
M[MUI v7]
|
||||
|
||||
P --> S
|
||||
P --> L
|
||||
S --> C
|
||||
C --> M
|
||||
L --> C
|
||||
L --> M
|
||||
C --> H
|
||||
S --> H
|
||||
M --> T
|
||||
```
|
||||
|
||||
- **Pages** are thin — they mount a section and pass URL params.
|
||||
- **Sections** are page-specific compositions of components + hooks + queries.
|
||||
- **Components** are reusable, free of business logic.
|
||||
- **Layouts** wrap pages with sidebar / topbar / auth scaffolding.
|
||||
|
||||
---
|
||||
|
||||
## 3. Token system
|
||||
|
||||
The theme exposes these token groups (see [[Theme Configuration]] for exact values):
|
||||
|
||||
| Group | Examples | Used as |
|
||||
|---|---|---|
|
||||
| **Palette** | `primary.main`, `error.dark`, `grey.500`, `background.paper` | `sx={{ color: 'primary.main' }}` |
|
||||
| **Typography** | `h1`-`h6`, `subtitle1/2`, `body1/2`, `caption`, `overline`, `button` | `<Typography variant="h2">` |
|
||||
| **Spacing** | unit = 8 px; use multiples: `spacing(1)` = 8, `spacing(2)` = 16, … | `sx={{ p: 2, mt: 3 }}` |
|
||||
| **Shape** | `borderRadius` = 8 (default), 12 for cards | `sx={{ borderRadius: 1 }}` |
|
||||
| **Shadows** | `shadows[1]`–`shadows[24]` + custom `customShadows` | `sx={{ boxShadow: 1 }}` |
|
||||
| **Breakpoints** | `xs`/`sm`/`md`/`lg`/`xl` = 0/600/900/1200/1536 | `sx={{ flexDirection: { xs: 'column', md: 'row' } }}` |
|
||||
| **Z-index** | named layers: `appBar`, `drawer`, `modal`, `snackbar`, `tooltip` | `sx={{ zIndex: 'modal' }}` |
|
||||
|
||||
The unit of spacing is **8 px**. Never use `padding: '14px'` — use `sx={{ p: 1.75 }}` if you really need it (or, better, fix the value to a multiple).
|
||||
|
||||
---
|
||||
|
||||
## 4. Modes
|
||||
|
||||
The app supports three independent user-controllable axes:
|
||||
|
||||
| Axis | Values | Persistence |
|
||||
|---|---|---|
|
||||
| Mode | `light` · `dark` · `system` | `localStorage.settings.mode` |
|
||||
| Contrast | `default` · `bold` | `localStorage.settings.contrast` |
|
||||
| Layout | `vertical` · `mini` · `horizontal` | `localStorage.settings.layout` |
|
||||
| Direction | `ltr` · `rtl` | `localStorage.settings.direction` (auto-set from locale) |
|
||||
| Color preset | one of N curated palettes | `localStorage.settings.colorPresets` |
|
||||
| Font family | per theme defaults or overridden | `localStorage.settings.fontFamily` |
|
||||
|
||||
Toggled from the **Settings Drawer** (`src/settings/drawer/`). See [[Settings & Theming]].
|
||||
|
||||
---
|
||||
|
||||
## 5. Folder structure
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── theme/
|
||||
│ ├── index.ts # createTheme + provider wiring
|
||||
│ ├── options/
|
||||
│ │ ├── palette.ts # Palette tokens (light + dark)
|
||||
│ │ ├── typography.ts # Type scale & families
|
||||
│ │ ├── shadows.ts # Elevation shadows
|
||||
│ │ ├── custom-shadows.ts # Brand-specific glow/elevation
|
||||
│ │ ├── overrides/ # MUI component overrides
|
||||
│ │ └── …
|
||||
│ └── styles/ # Global CSS resets, scrollbar
|
||||
├── settings/
|
||||
│ ├── context/ # SettingsContext provider
|
||||
│ └── drawer/ # The visible drawer UI
|
||||
├── components/ # ~50 reusable components (see below)
|
||||
└── layouts/
|
||||
├── auth-centered/
|
||||
├── auth-split/
|
||||
├── dashboard/ # sidebar + topbar
|
||||
└── main/ # public marketing-style
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Naming & code conventions (recap from cursor rules)
|
||||
|
||||
- File names: kebab-case (`custom-data-grid.tsx`).
|
||||
- Component names: PascalCase (`CustomDataGrid`).
|
||||
- Hook names: `use-*.ts` exporting `useXxx`.
|
||||
- Folder per component with `index.ts` barrel.
|
||||
- Import order: **Styles → Side-effects → Types → External libs → MUI → Internal**.
|
||||
- Forbidden: inline hex colors, `!important`, mixing `style={{}}` with `sx`.
|
||||
- Icons: **Iconify only** (`eva:*`, `solar:*` prefixes preferred).
|
||||
|
||||
---
|
||||
|
||||
## 7. Accessibility checklist
|
||||
|
||||
- All interactive elements reachable with `Tab`.
|
||||
- Focus visible (don't override default outline without replacement).
|
||||
- Icon-only buttons have `aria-label`.
|
||||
- Form fields linked to labels via `htmlFor` or wrapping `<label>`.
|
||||
- Color contrast verified at WCAG AA (`4.5:1` body text, `3:1` large text).
|
||||
- Live regions for snackbars (`aria-live="polite"`).
|
||||
- Skip-to-content link before the navbar.
|
||||
|
||||
---
|
||||
|
||||
## 8. RTL support
|
||||
|
||||
- The MUI emotion cache toggles `stylis-plugin-rtl` when `direction === 'rtl'`.
|
||||
- DataGrid honors a custom Persian locale at `locales/custom-fa-data-grid-locale.ts`.
|
||||
- Icon orientation (chevrons, arrows) is mirrored automatically by stylis-plugin-rtl.
|
||||
- Date pickers use `dayjs` with locale `fa`/`ar` loaded dynamically.
|
||||
|
||||
See [[Internationalization & RTL]] for the full setup.
|
||||
|
||||
---
|
||||
|
||||
## 9. Where to add new visuals
|
||||
|
||||
| Need | Add it to |
|
||||
|---|---|
|
||||
| New brand color | `theme/options/palette.ts` (light + dark) |
|
||||
| New text variant | `theme/options/typography.ts` |
|
||||
| MUI component override (default props, root sx) | `theme/options/overrides/<Component>.ts` |
|
||||
| Reusable widget | `src/components/<kebab-case-name>/` |
|
||||
| Page-specific composition | `src/sections/<feature>/<thing>.tsx` |
|
||||
| Layout variant | `src/layouts/<name>/` |
|
||||
| New language | `src/locales/langs/<code>/` + register in `locales-config.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Related
|
||||
|
||||
- [[Theme Configuration]] — exact MUI theme structure
|
||||
- [[Typography]] — type scale & font loading
|
||||
- [[Colors]] — palette tokens & contrast
|
||||
- [[Components]] — inventory of reusable components
|
||||
- [[Layouts]] — layout patterns
|
||||
- [[Internationalization & RTL]] — i18n + bidi
|
||||
- [[Iconography]] — Iconify usage rules
|
||||
- [[Settings & Theming]] — drawer-driven user prefs
|
||||
- [[Frontend Architecture]] — where this fits in the broader system
|
||||
- [[Coding Standards]] — full cursor-rules reference
|
||||
183
05 - Design System/Iconography.md
Normal file
183
05 - Design System/Iconography.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: Iconography
|
||||
tags: [design-system, icons, iconify]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Iconography
|
||||
|
||||
Icons come from **Iconify** — a unified SVG icon library aggregating 100k+ icons from dozens of icon sets, loaded on-demand.
|
||||
|
||||
> [!important]
|
||||
> Per cursor rules (`backend/.cursor/rules/ui-development-standards.mdc`), Iconify is the **only** icon system. Do NOT import from `@mui/icons-material`, `react-icons`, or inline SVG files unless you're building a wholly custom illustration.
|
||||
|
||||
---
|
||||
|
||||
## 1. Library
|
||||
|
||||
Installed as `@iconify/react`. Wrapped by `frontend/src/components/iconify/`:
|
||||
|
||||
```tsx
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
|
||||
<Iconify icon="solar:user-bold" />
|
||||
<Iconify icon="eva:close-fill" width={20} />
|
||||
<Iconify icon="mingcute:check-fill" sx={{ color: 'success.main' }} />
|
||||
```
|
||||
|
||||
Props: `icon` (required), `width` (number or string, default 20), `sx` (any MUI sx), plus standard HTML attributes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Approved icon sets
|
||||
|
||||
Per the cursor rules, **prefer** these prefixes:
|
||||
|
||||
| Prefix | Set | Style | Best for |
|
||||
|---|---|---|---|
|
||||
| `solar:` | Solar Icons | Bold / Line / Linear / Outline | Default everywhere — most consistent |
|
||||
| `eva:` | Eva Icons | Fill / Outline | Compact UI icons (close, more, plus) |
|
||||
| `mingcute:` | MingCute | Fill / Line | Detailed actions |
|
||||
| `mdi:` | Material Design Icons | Many variants | Use only when above sets lack the metaphor |
|
||||
| `simple-icons:` | Brand logos | Monochrome | Social platforms, third-party brands |
|
||||
| `flagpack:` | Country flags | Filled | Language switcher, address country |
|
||||
|
||||
> [!tip]
|
||||
> Pick ONE style per set across the app for visual consistency. The default convention is `solar:*-bold` for filled and `solar:*-line-duotone` for accents.
|
||||
|
||||
---
|
||||
|
||||
## 3. Sizing
|
||||
|
||||
| Size | Use |
|
||||
|---|---|
|
||||
| 16 px | Inline with text (caption, body2) |
|
||||
| 20 px | Inline with body1, navigation items, buttons (default) |
|
||||
| 24 px | Standalone action buttons, primary nav |
|
||||
| 32 px | Empty-state illustrations, large CTAs |
|
||||
| 48 px+ | Hero illustrations (consider Lottie or `react-icons` set built for hero use) |
|
||||
|
||||
Set via `width` (icon is always square):
|
||||
|
||||
```tsx
|
||||
<Iconify icon="solar:bell-bold" width={24} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Color
|
||||
|
||||
Color comes from the parent's `color` CSS property (Iconify renders with `fill: currentColor`). Use the `sx` prop:
|
||||
|
||||
```tsx
|
||||
<Iconify icon="solar:check-circle-bold" sx={{ color: 'success.main' }} />
|
||||
<Iconify icon="solar:warning-bold" sx={{ color: 'warning.dark' }} />
|
||||
```
|
||||
|
||||
Never hard-code hex — go through palette tokens.
|
||||
|
||||
---
|
||||
|
||||
## 5. Common icon mappings
|
||||
|
||||
A non-exhaustive convention table:
|
||||
|
||||
| Action / Concept | Recommended icon |
|
||||
|---|---|
|
||||
| Search | `solar:magnifer-bold` |
|
||||
| Close | `eva:close-fill` |
|
||||
| Add / Create | `solar:add-circle-bold` or `mingcute:add-fill` |
|
||||
| Edit | `solar:pen-bold` |
|
||||
| Delete | `solar:trash-bin-trash-bold` |
|
||||
| Settings | `solar:settings-bold` |
|
||||
| Notifications | `solar:bell-bold` (badge for unread) |
|
||||
| User / Profile | `solar:user-bold` |
|
||||
| Avatar group | `solar:users-group-rounded-bold` |
|
||||
| Shopping cart | `solar:cart-bold` |
|
||||
| Filter | `solar:filter-bold` |
|
||||
| Sort | `solar:sort-bold` |
|
||||
| More menu | `eva:more-vertical-fill` |
|
||||
| Back | `solar:alt-arrow-left-bold` (auto-flips RTL) |
|
||||
| Forward | `solar:alt-arrow-right-bold` |
|
||||
| Up / Down | `solar:alt-arrow-up-bold` / `solar:alt-arrow-down-bold` |
|
||||
| Success | `solar:check-circle-bold` |
|
||||
| Warning | `solar:danger-bold` |
|
||||
| Error | `solar:close-circle-bold` |
|
||||
| Info | `solar:info-circle-bold` |
|
||||
| Upload | `solar:upload-bold` |
|
||||
| Download | `solar:download-bold` |
|
||||
| Copy | `solar:copy-bold` |
|
||||
| Eye (show/hide password) | `solar:eye-bold` / `solar:eye-closed-bold` |
|
||||
| Wallet | `solar:wallet-money-bold` |
|
||||
| Coin / Token | `solar:dollar-bold`, `mingcute:coin-fill` |
|
||||
| QR code | `solar:qr-code-bold` |
|
||||
| Chat | `solar:chat-round-bold` |
|
||||
| Send (chat) | `solar:plain-bold` |
|
||||
| Attachment | `eva:attach-2-fill` |
|
||||
| Calendar | `solar:calendar-bold` |
|
||||
| Clock | `solar:clock-circle-bold` |
|
||||
| Location | `solar:map-point-bold` |
|
||||
| Phone | `solar:phone-bold` |
|
||||
| Email | `solar:letter-bold` |
|
||||
| Lock | `solar:lock-password-bold` |
|
||||
| Unlock | `solar:lock-unlock-bold` |
|
||||
| Star (rating) | `solar:star-bold` (filled) / `solar:star-linear` (empty) |
|
||||
| Hamburger | `solar:hamburger-menu-bold` |
|
||||
| Refresh | `solar:refresh-bold` |
|
||||
| Logout | `solar:logout-3-bold` |
|
||||
|
||||
---
|
||||
|
||||
## 6. RTL & direction
|
||||
|
||||
Direction-implying icons (chevrons, arrows, swipe) are flipped automatically by `stylis-plugin-rtl` for arrow-class icons. Semantic icons (play, share, send) should NOT flip — wrap them in:
|
||||
|
||||
```tsx
|
||||
<Box sx={{ direction: 'ltr' }}>
|
||||
<Iconify icon="solar:play-bold" />
|
||||
</Box>
|
||||
```
|
||||
|
||||
Or use `transform: 'scaleX(-1)'` on RTL if you specifically need to mirror.
|
||||
|
||||
---
|
||||
|
||||
## 7. Custom SVG (when Iconify is not enough)
|
||||
|
||||
Place SVGs in `public/assets/icons/` or `frontend/src/assets/icons/` and use:
|
||||
|
||||
```tsx
|
||||
import { SvgColor } from 'src/components/svg-color';
|
||||
|
||||
<SvgColor src="/assets/icons/custom-shape.svg" sx={{ color: 'primary.main' }} />
|
||||
```
|
||||
|
||||
`svg-color` uses the mask-image technique so a single-color SVG inherits the parent color, matching Iconify behavior.
|
||||
|
||||
For multi-color illustrations, ship them as separate files in `public/illustrations/` and reference with Next `Image`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Accessibility
|
||||
|
||||
- Icon-only buttons MUST have `aria-label`:
|
||||
```tsx
|
||||
<IconButton aria-label="close"><Iconify icon="eva:close-fill" /></IconButton>
|
||||
```
|
||||
- Icons next to a label are decorative — no aria-label needed.
|
||||
- Live status icons (loading, success) inside a `aria-live="polite"` region announce changes.
|
||||
|
||||
---
|
||||
|
||||
## 9. Performance
|
||||
|
||||
- Iconify loads icons on demand from its CDN (or self-hosted if configured).
|
||||
- Bundle impact is minimal — only the icon component, not the icon data.
|
||||
- For the most-used set (e.g., `solar:*`), consider pre-loading via `@iconify-icons/solar` to remove the CDN dependency.
|
||||
|
||||
---
|
||||
|
||||
## 10. Related
|
||||
|
||||
- [[Design System Overview]] · [[Components]] · [[Colors]]
|
||||
- [[Coding Standards]] — Iconify rule cited
|
||||
276
05 - Design System/Internationalization & RTL.md
Normal file
276
05 - Design System/Internationalization & RTL.md
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
title: Internationalization & RTL
|
||||
tags: [design-system, i18n, rtl, localization]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Internationalization & RTL
|
||||
|
||||
The frontend supports six languages with full LTR/RTL bidi handling. Configuration lives in `frontend/src/locales/`.
|
||||
|
||||
| Code | Language | Direction | Native name |
|
||||
|---|---|---|---|
|
||||
| `en` | English | LTR | English |
|
||||
| `fa` | Persian (Farsi) | **RTL** | فارسی |
|
||||
| `ar` | Arabic | **RTL** | العربية |
|
||||
| `fr` | French | LTR | Français |
|
||||
| `cn` | Chinese (Simplified) | LTR | 简体中文 |
|
||||
| `vi` | Vietnamese | LTR | Tiếng Việt |
|
||||
|
||||
---
|
||||
|
||||
## 1. Library stack
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `i18next` | Core i18n engine |
|
||||
| `react-i18next` | React bindings (`useTranslation`, `<Trans>`) |
|
||||
| `i18next-browser-languagedetector` | Detect from `navigator.language` / cookie |
|
||||
| `stylis-plugin-rtl` | Auto-flip CSS for RTL |
|
||||
| `dayjs` | Date formatting with locale support |
|
||||
| `@mui/x-date-pickers/AdapterDayjs` | Picker UI honors locale |
|
||||
| `Intl.NumberFormat` (built-in) | Numeric formatting |
|
||||
|
||||
---
|
||||
|
||||
## 2. Setup (`src/locales/locales-config.ts`)
|
||||
|
||||
```ts
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enCommon from './langs/en/common.json';
|
||||
import faCommon from './langs/fa/common.json';
|
||||
import arCommon from './langs/ar/common.json';
|
||||
// ... fr, cn, vi
|
||||
|
||||
export const allLangs = [
|
||||
{ value: 'en', label: 'English', dir: 'ltr', icon: 'flagpack:gb-ukm' },
|
||||
{ value: 'fa', label: 'فارسی', dir: 'rtl', icon: 'flagpack:ir' },
|
||||
{ value: 'ar', label: 'العربية', dir: 'rtl', icon: 'flagpack:sa' },
|
||||
{ value: 'fr', label: 'Français', dir: 'ltr', icon: 'flagpack:fr' },
|
||||
{ value: 'cn', label: '简体中文', dir: 'ltr', icon: 'flagpack:cn' },
|
||||
{ value: 'vi', label: 'Tiếng Việt', dir: 'ltr', icon: 'flagpack:vn' },
|
||||
];
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { common: enCommon },
|
||||
fa: { common: faCommon },
|
||||
ar: { common: arCommon },
|
||||
fr: { common: frCommon },
|
||||
cn: { common: cnCommon },
|
||||
vi: { common: viCommon },
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
defaultNS: 'common',
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Translation files
|
||||
|
||||
```
|
||||
src/locales/langs/
|
||||
├── en/common.json
|
||||
├── fa/common.json
|
||||
├── ar/common.json
|
||||
├── fr/common.json
|
||||
├── cn/common.json
|
||||
└── vi/common.json
|
||||
```
|
||||
|
||||
Convention: one file per **namespace**. `common.json` is the default; split if any namespace exceeds ~500 keys. Examples (key paths):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"actions": { "save": "Save", "cancel": "Cancel", "delete": "Delete" },
|
||||
"auth": { "signIn": "Sign in", "signOut": "Sign out", "rememberMe": "Remember me" },
|
||||
"marketplace": {
|
||||
"request": {
|
||||
"create": "Create a request",
|
||||
"openOffers": "{{count}} open offer",
|
||||
"openOffers_plural": "{{count}} open offers"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Usage in components
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function CreateButton() {
|
||||
const { t } = useTranslation();
|
||||
return <Button>{t('marketplace.request.create')}</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
For sentences with embedded React (link, bold) use `<Trans>`:
|
||||
|
||||
```tsx
|
||||
<Trans i18nKey="legal.terms">
|
||||
By signing up you agree to our <Link href="/terms">Terms</Link>.
|
||||
</Trans>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Direction (LTR / RTL)
|
||||
|
||||
Triggered by the active locale (`dir: 'rtl'` for `fa` / `ar`).
|
||||
|
||||
### 5.1 Cache swap
|
||||
|
||||
```tsx
|
||||
const cacheKey = direction === 'rtl' ? 'css-rtl' : 'css';
|
||||
const stylisPlugins = direction === 'rtl' ? [rtlPlugin] : [];
|
||||
|
||||
<AppRouterCacheProvider options={{ key: cacheKey, prepend: true, stylisPlugins }}>
|
||||
{children}
|
||||
</AppRouterCacheProvider>
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> The cache key **must differ** between modes. Sharing the key causes stale CSS to linger when direction changes at runtime.
|
||||
|
||||
### 5.2 Theme
|
||||
|
||||
```ts
|
||||
const theme = createTheme({ direction, ... });
|
||||
```
|
||||
|
||||
### 5.3 `<html dir>`
|
||||
|
||||
The root layout sets `<html lang={locale} dir={direction}>`. Some screen readers and form behaviors (e.g., number input flow) rely on this attribute.
|
||||
|
||||
---
|
||||
|
||||
## 6. RTL CSS rules
|
||||
|
||||
| Don't | Do |
|
||||
|---|---|
|
||||
| `margin-left: 8px` | `marginInlineStart: 1` (or `ml: 1` — MUI's `sx` translates) |
|
||||
| `padding-right: 16px` | `paddingInlineEnd: 2` |
|
||||
| `text-align: left` | `textAlign: 'start'` |
|
||||
| `float: left` | `float: 'inline-start'` (or refactor) |
|
||||
| `border-left: 2px` | `borderInlineStart: 2` |
|
||||
| `direction: ltr` hardcoded | inherit from html unless intentional (e.g., code blocks always LTR) |
|
||||
|
||||
Icons that imply direction (chevrons, arrows) are auto-flipped by `stylis-plugin-rtl`. If an icon is **semantic** (e.g., a play button), wrap it in:
|
||||
|
||||
```tsx
|
||||
<Box sx={{ direction: 'ltr' }}>
|
||||
<Iconify icon="solar:play-bold" />
|
||||
</Box>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Date & time formatting
|
||||
|
||||
```tsx
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/fa';
|
||||
dayjs.locale('fa');
|
||||
|
||||
dayjs('2026-05-23').format('LL'); // → "۲ خرداد ۱۴۰۵"
|
||||
```
|
||||
|
||||
Use the `Format` helpers from `components/...` for consistent display patterns:
|
||||
|
||||
| Helper | Output |
|
||||
|---|---|
|
||||
| `fDate(d)` | locale-aware short date |
|
||||
| `fDateTime(d)` | date + time |
|
||||
| `fTime(d)` | time only |
|
||||
| `fToNow(d)` | "3 hours ago" |
|
||||
| `fIsBetween(start, end)` | boolean window check |
|
||||
|
||||
---
|
||||
|
||||
## 8. Number & currency formatting
|
||||
|
||||
```ts
|
||||
import { fNumber, fCurrency, fPercent } from '@/locales/utils/number-format-locale';
|
||||
|
||||
fNumber(1234567); // "1,234,567" / "۱٬۲۳۴٬۵۶۷"
|
||||
fCurrency(99.5, 'USD'); // "$99.50" / "۹۹٫۵۰ دلار"
|
||||
fPercent(0.123); // "12.3%"
|
||||
```
|
||||
|
||||
Underneath, these wrap `Intl.NumberFormat` with the active locale, falling back to `en-US` when the formatter doesn't support a locale (e.g., older browsers + `fa`).
|
||||
|
||||
---
|
||||
|
||||
## 9. DataGrid Farsi locale
|
||||
|
||||
`src/locales/custom-fa-data-grid-locale.ts` exports a Persian translation map for MUI X DataGrid (column menu, pagination labels, filter operators, etc.). Wired in via:
|
||||
|
||||
```tsx
|
||||
<DataGrid
|
||||
localeText={locale === 'fa' ? customFaDataGrid : undefined}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
For Arabic, the built-in MUI `arSD` locale works.
|
||||
|
||||
---
|
||||
|
||||
## 10. Language switcher UI
|
||||
|
||||
In the topbar, a chip with the flag + native name opens a popover listing all `allLangs`. On click:
|
||||
|
||||
1. `i18n.changeLanguage(code)`
|
||||
2. `settings.onUpdate('direction', dir)` ← persists into settings context
|
||||
3. Page does NOT reload — the cache + theme + provider all react reactively.
|
||||
|
||||
---
|
||||
|
||||
## 11. Adding a new language
|
||||
|
||||
1. Create `src/locales/langs/<code>/common.json` with the same key set as `en/common.json` (use `i18next-parser` to identify missing keys).
|
||||
2. Register in `allLangs` array in `locales-config.ts`.
|
||||
3. Add `dayjs` locale import if it differs from the existing set.
|
||||
4. Verify number formatter outputs on a sample page.
|
||||
5. If RTL, add `direction: 'rtl'` and **test bidi** thoroughly.
|
||||
|
||||
---
|
||||
|
||||
## 12. Translation workflow
|
||||
|
||||
- Source of truth: `en/common.json` (English).
|
||||
- For new strings, add to `en/` first, then translate to other locales (manual or via Crowdin / Lokalise integration).
|
||||
- Lint check: `i18next-parser` can compare locale files for missing keys.
|
||||
|
||||
---
|
||||
|
||||
## 13. Common pitfalls
|
||||
|
||||
| Pitfall | Fix |
|
||||
|---|---|
|
||||
| Hardcoded English in JSX | Move to `common.json` |
|
||||
| `align="left"` on a Box | Use `textAlign="start"` |
|
||||
| `marginLeft` instead of `ml` shorthand | Use shorthand (auto-RTL) |
|
||||
| Forgot to load Persian font | Add to `theme/options/typography.ts` |
|
||||
| Date showing as English digits in fa | Forgot `dayjs.locale('fa')` |
|
||||
| Numbers in RTL appearing reversed | Wrap in `<bdi>` |
|
||||
|
||||
---
|
||||
|
||||
## 14. Related
|
||||
|
||||
- [[Design System Overview]] · [[Theme Configuration]] · [[Typography]]
|
||||
- [[Settings & Theming]] — direction toggle
|
||||
- [[Frontend Architecture]] — provider tree
|
||||
- [[Roles & Personas]] — locale defaults per role (admin defaults English, buyers can be Persian)
|
||||
217
05 - Design System/Layouts.md
Normal file
217
05 - Design System/Layouts.md
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: Layouts
|
||||
tags: [design-system, layout, ui]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Layouts
|
||||
|
||||
Page-template wrappers in `frontend/src/layouts/`. Each layout encapsulates a header, optional sidebar, content slot, and footer — pages mount inside one and never re-implement chrome.
|
||||
|
||||
---
|
||||
|
||||
## 1. Layout variants
|
||||
|
||||
| Layout | Used for | Header | Sidebar | Footer |
|
||||
|---|---|---|---|---|
|
||||
| `auth-centered/` | Sign-in, sign-up, verify, reset-password | Logo only | none | Compact links |
|
||||
| `auth-split/` | Marketing-leaning auth (sign-up with promo panel) | Logo only | Left visual panel | none |
|
||||
| `dashboard/` | Every `/dashboard/*` route | Topbar with user menu + notifications + language + settings | Vertical / Mini / Horizontal | none |
|
||||
| `main/` | Public marketing pages (`shop/`, `post/`, landing) | Marketing nav | none | Full footer |
|
||||
|
||||
---
|
||||
|
||||
## 2. Dashboard layout
|
||||
|
||||
`layouts/dashboard/` is the workhorse. It uses MUI's `LayoutSection` pattern — slots for header, sidebar, main, footer with media-query-aware responsive behavior.
|
||||
|
||||
### 2.1 Anatomy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ TopBar ───────────────────────── search bell ▾ │ ← `header-section`
|
||||
├─────────┬───────────────────────────────────────────┤
|
||||
│ Nav │ Breadcrumbs │
|
||||
│ │ ┌─ Page content (mounted route) ──────┐ │
|
||||
│ ┌───┐ │ │ │ │
|
||||
│ │ ○ │ │ │ │ │
|
||||
│ └───┘ │ │ │ │
|
||||
│ user │ │ │ │
|
||||
│ profile│ │ │ │
|
||||
├─────────┴───────────────────────────────────────────┤
|
||||
│ (optional) Footer slot — unused in dashboard │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Sidebar variants
|
||||
|
||||
User can switch via the **Settings Drawer**:
|
||||
|
||||
| Variant | Width | Behavior |
|
||||
|---|---|---|
|
||||
| `vertical` | 280 px | Default; icon + label, collapsible groups |
|
||||
| `mini` | 88 px | Icons only; labels in tooltip on hover |
|
||||
| `horizontal` | full-width below header | Nav as a horizontal menu — main content below |
|
||||
|
||||
### 2.3 Topbar
|
||||
|
||||
Contains, left-to-right:
|
||||
|
||||
- Sidebar toggle button (mobile / mini-mode)
|
||||
- Logo (mini, shown only when sidebar is collapsed)
|
||||
- Search (Cmd+K palette) — optional
|
||||
- Language selector (chip with flag)
|
||||
- Settings drawer toggle (cog icon)
|
||||
- Notifications drawer toggle (bell with unread count badge)
|
||||
- Account dropdown (avatar → profile, sign-out)
|
||||
|
||||
### 2.4 Nav configuration
|
||||
|
||||
Nav items defined as a tree in a config file (e.g., `layouts/dashboard/config-nav-dashboard.ts`):
|
||||
|
||||
```ts
|
||||
const navData = [
|
||||
{
|
||||
subheader: 'Overview',
|
||||
items: [
|
||||
{ title: 'Dashboard', path: paths.dashboard.root, icon: ICONS.dashboard },
|
||||
{ title: 'Chat', path: paths.dashboard.chat, icon: ICONS.chat },
|
||||
],
|
||||
},
|
||||
{
|
||||
subheader: 'Marketplace',
|
||||
items: [
|
||||
{ title: 'Requests', path: paths.dashboard.request.root, icon: ICONS.request,
|
||||
roles: ['buyer', 'admin'] },
|
||||
{ title: 'Templates', path: paths.dashboard.template.root, icon: ICONS.template,
|
||||
roles: ['seller', 'admin'] },
|
||||
// ...
|
||||
],
|
||||
},
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Each `items[i]` may carry `roles: string[]` to scope visibility. The nav renderer filters items based on the current user's role before rendering — entries without `roles` are shown to everyone.
|
||||
|
||||
### 2.5 Breadcrumbs
|
||||
|
||||
`components/custom-breadcrumbs` renders the active route trail plus an optional action button slot (e.g., "+ New Request"). Convention: set via the page component.
|
||||
|
||||
---
|
||||
|
||||
## 3. Auth layouts
|
||||
|
||||
### 3.1 `auth-centered/`
|
||||
|
||||
Single column, centered card. Used by sign-in, password reset, email verification.
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Logo │
|
||||
│ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ Title │ │
|
||||
│ │ Form fields... │ │
|
||||
│ │ CTA │ │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ Tiny footer links │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 `auth-split/`
|
||||
|
||||
Two-column on `md+`, single column on small screens. Left = visual / brand panel; right = form.
|
||||
|
||||
```
|
||||
┌──────────────┬───────────────┐
|
||||
│ │ Logo │
|
||||
│ Visual │ │
|
||||
│ panel │ Form │
|
||||
│ (image, │ │
|
||||
│ quote, │ CTA │
|
||||
│ benefit) │ │
|
||||
│ │ Footer │
|
||||
└──────────────┴───────────────┘
|
||||
```
|
||||
|
||||
Used for sign-up to balance marketing copy with the form.
|
||||
|
||||
---
|
||||
|
||||
## 4. Main (public) layout
|
||||
|
||||
`layouts/main/` is for unauthenticated visitor pages. Sticky header, then content, then full footer.
|
||||
|
||||
Header includes:
|
||||
|
||||
- Logo
|
||||
- Top-level nav (Shop, Blog, About)
|
||||
- Language switcher
|
||||
- "Sign in" / "Sign up" buttons (or "Dashboard" if signed in)
|
||||
|
||||
Footer includes:
|
||||
|
||||
- Brand block
|
||||
- Sitemap
|
||||
- Legal links
|
||||
- Social media
|
||||
- Newsletter signup (optional)
|
||||
|
||||
---
|
||||
|
||||
## 5. Responsive behavior
|
||||
|
||||
| Breakpoint | xs (<600) | sm (≥600) | md (≥900) | lg (≥1200) | xl (≥1536) |
|
||||
|---|---|---|---|---|---|
|
||||
| Dashboard sidebar | drawer overlay | drawer overlay | mini default | full default | full default |
|
||||
| Auth-split | single col | single col | two col | two col | two col |
|
||||
| Main nav | hamburger | hamburger | inline | inline | inline |
|
||||
| Page padding | 16 px | 16 px | 24 px | 32 px | 32 px |
|
||||
|
||||
Implementation note: do NOT add `@media` rules in components. Use the responsive `sx` syntax:
|
||||
|
||||
```tsx
|
||||
<Box sx={{ p: { xs: 2, md: 3, lg: 4 } }} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Sticky elements
|
||||
|
||||
- TopBar uses `position: sticky; top: 0; z-index: appBar` so it stays on scroll.
|
||||
- Action bars on long forms can use the same pattern at bottom (`bottom: 0`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Skeleton & loading
|
||||
|
||||
- Each `dashboard/*/page.tsx` can ship a sibling `loading.tsx` that renders skeleton placeholders matching the final layout.
|
||||
- For long-loading sections, use `<Suspense fallback={<SkeletonX />}>`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Error boundaries
|
||||
|
||||
- App-level error boundary at root layout catches uncaught render errors.
|
||||
- Each route segment may ship `error.tsx` for scoped recovery (e.g., `dashboard/shops/error.tsx`).
|
||||
- Snackbars handle non-fatal API errors.
|
||||
|
||||
---
|
||||
|
||||
## 9. Adding a new layout
|
||||
|
||||
1. Create `layouts/<name>/` with `index.tsx` (default export the layout component).
|
||||
2. Slot pattern: accept `children`; render header + nav + main + (optional) footer.
|
||||
3. Reuse `components/scrollbar` for the main scroll area to keep custom scrollbar styling.
|
||||
4. If it's an auth flow, wire it into `app/auth/<flow>/layout.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Related
|
||||
|
||||
- [[Design System Overview]] · [[Theme Configuration]] · [[Components]]
|
||||
- [[Frontend Architecture]] — where layouts sit in the tree
|
||||
- [[Roles & Personas]] — role-driven nav filtering
|
||||
- [[Settings & Theming]] — layout variant switcher
|
||||
222
05 - Design System/Settings & Theming.md
Normal file
222
05 - Design System/Settings & Theming.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
title: Settings & Theming
|
||||
tags: [design-system, settings, theming]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Settings & Theming
|
||||
|
||||
A drawer-based UI lets the end user toggle visual preferences. Settings persist in `localStorage` and rebuild the MUI theme on the fly.
|
||||
|
||||
> [!info]
|
||||
> Implementation: `frontend/src/settings/context/` (state) + `frontend/src/settings/drawer/` (UI) + `frontend/src/components/settings/` (helpers).
|
||||
|
||||
---
|
||||
|
||||
## 1. What's user-controllable
|
||||
|
||||
| Axis | Values | Default | Persisted |
|
||||
|---|---|---|---|
|
||||
| **Mode** | `light` · `dark` · `system` | `system` | localStorage |
|
||||
| **Contrast** | `default` · `bold` | `default` | localStorage |
|
||||
| **Layout** | `vertical` · `mini` · `horizontal` | `vertical` | localStorage |
|
||||
| **Direction** | `ltr` · `rtl` | derived from locale | localStorage (overrides locale default) |
|
||||
| **Color preset** | one of `default`, `purple`, `cyan`, `blue`, `orange`, `red` | `default` | localStorage |
|
||||
| **Font family** | `Public Sans Variable`, `DM Sans Variable`, `Inter Variable`, `Nunito Sans Variable` | `Public Sans Variable` | localStorage |
|
||||
| **Compact navigation** | boolean | `false` | localStorage |
|
||||
| **Border radius** | 0–24 | 8 | localStorage |
|
||||
| **Stretched container** | boolean | `false` | localStorage |
|
||||
|
||||
---
|
||||
|
||||
## 2. State shape
|
||||
|
||||
```ts
|
||||
// frontend/src/settings/types.ts (approx)
|
||||
export interface Settings {
|
||||
mode: 'light' | 'dark' | 'system';
|
||||
contrast: 'default' | 'bold';
|
||||
layout: 'vertical' | 'mini' | 'horizontal';
|
||||
direction: 'ltr' | 'rtl';
|
||||
colorPreset: string;
|
||||
fontFamily: string;
|
||||
compactLayout: boolean;
|
||||
primaryColor?: string;
|
||||
stretch: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Stored in a single `localStorage` key (`settings` or `settings-key`). The context provider hydrates on mount, falls back to defaults if absent.
|
||||
|
||||
---
|
||||
|
||||
## 3. Context API
|
||||
|
||||
```ts
|
||||
// frontend/src/settings/context/use-settings.ts (approx)
|
||||
export interface SettingsContext {
|
||||
state: Settings;
|
||||
canReset: boolean;
|
||||
onReset(): void;
|
||||
onUpdate<K extends keyof Settings>(key: K, value: Settings[K]): void;
|
||||
onUpdateField<K extends keyof Settings>(key: K, value: Settings[K]): void;
|
||||
openDrawer: boolean;
|
||||
onToggleDrawer(): void;
|
||||
}
|
||||
|
||||
const { state, onUpdate, onToggleDrawer } = useSettings();
|
||||
```
|
||||
|
||||
Use `onUpdate` to change any value. The context fires a re-render that:
|
||||
1. Updates localStorage.
|
||||
2. Triggers a new `buildTheme()` invocation up at `ThemeProvider`.
|
||||
3. Components re-render with the new theme tokens.
|
||||
|
||||
---
|
||||
|
||||
## 4. Drawer UI
|
||||
|
||||
The drawer (`settings/drawer/SettingsDrawer.tsx`) is a right-side `MuiDrawer` with sections:
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Settings [reset] │
|
||||
├──────────────────────────────┤
|
||||
│ Mode [☀] [🌙] [⌐] │
|
||||
│ │
|
||||
│ Contrast ◯ Default ◯ Bold │
|
||||
│ │
|
||||
│ Direction ⬅ LTR ➡ RTL │
|
||||
│ │
|
||||
│ Layout ▤ Vertical │
|
||||
│ ▣ Mini │
|
||||
│ ━ Horizontal │
|
||||
│ │
|
||||
│ Color [●●●●●●] │
|
||||
│ │
|
||||
│ Font [Public Sans ▼] │
|
||||
│ │
|
||||
│ Border radius [—•—] 8 │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
Each section uses the `BlockOption` helper component for consistent styling.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wiring at the root
|
||||
|
||||
```tsx
|
||||
// frontend/src/app/layout.tsx (simplified)
|
||||
<SettingsProvider defaultSettings={defaults}>
|
||||
<ThemeProviderFromSettings>
|
||||
{children}
|
||||
<SettingsDrawer />
|
||||
</ThemeProviderFromSettings>
|
||||
</SettingsProvider>
|
||||
```
|
||||
|
||||
`ThemeProviderFromSettings` reads the settings context and rebuilds the theme on every change.
|
||||
|
||||
---
|
||||
|
||||
## 6. Locale ↔ direction coupling
|
||||
|
||||
By default, switching to `fa` / `ar` flips `direction: 'rtl'`. The user CAN override (a Persian-speaker on a desktop might prefer LTR sometimes).
|
||||
|
||||
```ts
|
||||
// In a settings change handler:
|
||||
onUpdate('direction', i18n.language === 'fa' ? 'rtl' : 'ltr');
|
||||
```
|
||||
|
||||
Or let the user pin direction independently — saved direction wins over locale-derived direction.
|
||||
|
||||
---
|
||||
|
||||
## 7. Mode = `system`
|
||||
|
||||
When `mode === 'system'`:
|
||||
|
||||
```ts
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const effectiveMode = prefersDark ? 'dark' : 'light';
|
||||
```
|
||||
|
||||
Subscribe to changes:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (settings.mode !== 'system') return;
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const onChange = () => forceUpdate();
|
||||
mql.addEventListener('change', onChange);
|
||||
return () => mql.removeEventListener('change', onChange);
|
||||
}, [settings.mode]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Color presets
|
||||
|
||||
Defined in `frontend/src/theme/options/presets/` or similar. Each preset only swaps `primary` (and optionally `secondary`) — `grey`, semantic colors, and background stay constant so layouts feel consistent across presets.
|
||||
|
||||
Adding a preset:
|
||||
|
||||
1. Add a `<name>.ts` exporting `{ lighter, light, main, dark, darker, contrastText }`.
|
||||
2. Register in `colorPresets` map.
|
||||
3. Add a swatch entry in the drawer's color picker block.
|
||||
|
||||
---
|
||||
|
||||
## 9. Font preset
|
||||
|
||||
Switching the font family applies at the theme level:
|
||||
|
||||
```ts
|
||||
typography: { fontFamily: settings.fontFamily, ... }
|
||||
```
|
||||
|
||||
If a font isn't bundled, dynamically `import('@fontsource-variable/<name>')` first to avoid FOUC.
|
||||
|
||||
---
|
||||
|
||||
## 10. Compact / stretched layout
|
||||
|
||||
| Setting | Effect |
|
||||
|---|---|
|
||||
| `compactLayout: true` | Reduces top spacing / padding on dashboard pages |
|
||||
| `stretch: true` | Removes the centered max-width on content (full-bleed) |
|
||||
|
||||
---
|
||||
|
||||
## 11. Border radius
|
||||
|
||||
Slider exposed as 0 → 24. Affects `theme.shape.borderRadius`, cascading to every component using `sx={{ borderRadius: 1 }}` semantics.
|
||||
|
||||
---
|
||||
|
||||
## 12. Reset to defaults
|
||||
|
||||
`onReset()` clears localStorage and re-hydrates with the default object. `canReset` is true when current state differs from defaults — used to enable/disable the Reset button.
|
||||
|
||||
---
|
||||
|
||||
## 13. Hydration mismatch hazard
|
||||
|
||||
Because settings live in `localStorage`, the server render can't know them. The provider implements a 2-pass strategy:
|
||||
|
||||
1. First render uses **defaults** (matches what the server emits → no hydration mismatch).
|
||||
2. After mount, the provider reads localStorage and re-renders with the user's settings.
|
||||
|
||||
Brief flash possible. To mitigate, either:
|
||||
- Suppress the first paint (split layout into client-only).
|
||||
- Or set the user's settings into a cookie at sign-in so server can pre-render correctly.
|
||||
|
||||
---
|
||||
|
||||
## 14. Related
|
||||
|
||||
- [[Design System Overview]] · [[Theme Configuration]] · [[Colors]] · [[Typography]]
|
||||
- [[Layouts]] — layout variants drive sidebar
|
||||
- [[Internationalization & RTL]] — direction coupling
|
||||
- [[Frontend Architecture]] — provider tree
|
||||
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]]
|
||||
186
05 - Design System/Typography.md
Normal file
186
05 - Design System/Typography.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: Typography
|
||||
tags: [design-system, typography, fonts]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Typography
|
||||
|
||||
The system uses **Public Sans Variable** as the primary face with **Barlow** as a secondary (display) face, plus locale-specific Persian/Arabic faces loaded when the active language requires them.
|
||||
|
||||
---
|
||||
|
||||
## 1. Font stack
|
||||
|
||||
Loaded via `@fontsource-variable` (variable fonts streamed at build) plus `@fontsource/barlow`. Confirm in `frontend/package.json`:
|
||||
|
||||
```jsonc
|
||||
"@fontsource-variable/public-sans": "^5.2.5", // Primary
|
||||
"@fontsource-variable/dm-sans": "^5.2.5", // Optional preset
|
||||
"@fontsource-variable/inter": "^5.2.5", // Optional preset
|
||||
"@fontsource-variable/nunito-sans": "^5.2.5", // Optional preset
|
||||
"@fontsource/barlow": "^5.2.5", // Secondary (display)
|
||||
```
|
||||
|
||||
Imported in `frontend/src/app/layout.tsx` (or a fonts module) so Next can fingerprint and preload them.
|
||||
|
||||
Default font-family stack in the theme:
|
||||
|
||||
```css
|
||||
font-family: "Public Sans Variable", "Helvetica", "Arial", sans-serif;
|
||||
```
|
||||
|
||||
Display-only headings (banners, hero) may override with Barlow via the `sx` prop:
|
||||
|
||||
```tsx
|
||||
<Typography variant="h1" sx={{ fontFamily: '"Barlow", serif' }}>Welcome</Typography>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Type scale
|
||||
|
||||
| Variant | Size (px) | Line height | Weight | Use |
|
||||
|---|---|---|---|---|
|
||||
| `h1` | 64 | 80 | 800 | Hero titles only |
|
||||
| `h2` | 48 | 64 | 800 | Page titles |
|
||||
| `h3` | 32 | 48 | 700 | Section titles |
|
||||
| `h4` | 24 | 36 | 700 | Card titles, dialog headers |
|
||||
| `h5` | 20 | 30 | 700 | Sub-section titles |
|
||||
| `h6` | 18 | 28 | 700 | Item titles, sidebar headers |
|
||||
| `subtitle1` | 16 | 24 | 600 | Form section labels |
|
||||
| `subtitle2` | 14 | 22 | 600 | List item subtitles |
|
||||
| `body1` | 16 | 24 | 400 | Body copy default |
|
||||
| `body2` | 14 | 22 | 400 | Secondary copy, table cells |
|
||||
| `caption` | 12 | 18 | 400 | Helper text, timestamps |
|
||||
| `overline` | 12 | 18 | 700 | Tags, all-caps category labels |
|
||||
| `button` | 14 | 24 | 700 | Button text — NOT uppercase |
|
||||
|
||||
> [!tip]
|
||||
> Use `responsiveFontSizes(theme)` once at theme creation to scale `h1`/`h2` on small screens automatically.
|
||||
|
||||
---
|
||||
|
||||
## 3. Weights
|
||||
|
||||
The variable Public Sans face supports 100–900. Convention:
|
||||
|
||||
| Weight | Token | Use |
|
||||
|---|---|---|
|
||||
| 400 | `fontWeightRegular` | Body |
|
||||
| 500 | `fontWeightMedium` | Mild emphasis |
|
||||
| 600 | `fontWeightSemiBold` | Subtitles, table headers |
|
||||
| 700 | `fontWeightBold` | Headings |
|
||||
| 800 | (raw) | Display |
|
||||
|
||||
Never use `font-weight: bolder` — always pick from the table.
|
||||
|
||||
---
|
||||
|
||||
## 4. Letter-spacing
|
||||
|
||||
- Headings (h1–h2): `-1` (tight, modern)
|
||||
- Overline: `+1.1` (open, all-caps)
|
||||
- Body, subtitle: `0`
|
||||
|
||||
---
|
||||
|
||||
## 5. Numeric variants
|
||||
|
||||
Tabular figures matter for financial UI (payments, prices). Enable per-instance:
|
||||
|
||||
```tsx
|
||||
<Typography variant="body1" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
{amount}
|
||||
</Typography>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Multi-script support
|
||||
|
||||
The system officially supports Latin, **Persian (Farsi)**, and **Arabic** rendering; French and Vietnamese use Latin glyphs already covered by Public Sans; Chinese (Simplified) falls back to the OS-default CJK font (the current fontsource set does not ship CJK).
|
||||
|
||||
Persian/Arabic font swap is layered at theme build:
|
||||
|
||||
```ts
|
||||
fontFamily:
|
||||
locale === 'fa' || locale === 'ar'
|
||||
? '"Vazirmatn", "Public Sans Variable", "Tahoma", sans-serif'
|
||||
: '"Public Sans Variable", "Helvetica", sans-serif'
|
||||
```
|
||||
|
||||
> [!note]
|
||||
> "Vazirmatn" is the **recommended** Persian face for new installations — it has full Unicode coverage and a variable axis. If not installed via `@fontsource`, ship it as a self-hosted woff2 under `public/fonts/`.
|
||||
|
||||
For Chinese (`cn`), consider adding `"PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"` to the family at the front.
|
||||
|
||||
For Vietnamese (`vi`) — Public Sans already covers all needed glyphs.
|
||||
|
||||
---
|
||||
|
||||
## 7. Line-length
|
||||
|
||||
Aim for **45–75 characters per line** on body copy. Constrain with `maxWidth`:
|
||||
|
||||
```tsx
|
||||
<Typography variant="body1" sx={{ maxWidth: 720 }}>{longBody}</Typography>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Text truncation
|
||||
|
||||
Single-line:
|
||||
|
||||
```tsx
|
||||
<Typography noWrap>{text}</Typography>
|
||||
```
|
||||
|
||||
Multi-line (clamp at 2 lines):
|
||||
|
||||
```tsx
|
||||
<Typography
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>{text}</Typography>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Bidi & RTL considerations
|
||||
|
||||
- Don't hard-code `text-align: left/right` — use `text-align: start/end` so it flips with direction.
|
||||
- For inline mixed-script (Persian + English), prefer the auto direction inside `<bdi>` or wrap with the Unicode `` (RLE) / `` (PDF) markers.
|
||||
- Numeric formatting per locale handled by `Intl.NumberFormat`; do NOT translate digits manually.
|
||||
|
||||
---
|
||||
|
||||
## 10. Loading & FOUC prevention
|
||||
|
||||
- All fonts loaded at the route segment that needs them (Next.js best practice).
|
||||
- `font-display: swap` is the default in fontsource — visible text uses fallback first, then swaps.
|
||||
- For the FIRST PAINT on auth pages, preload the primary Latin face by adding `<link rel="preload" as="font" />` to the root layout.
|
||||
|
||||
---
|
||||
|
||||
## 11. Customising
|
||||
|
||||
Add a font:
|
||||
|
||||
1. `yarn add @fontsource-variable/<name>`
|
||||
2. `import '@fontsource-variable/<name>'` in `src/app/layout.tsx`.
|
||||
3. Add to `fontFamily` stack in `theme/options/typography.ts`.
|
||||
4. Register as a settings option if user-selectable.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[Theme Configuration]] · [[Design System Overview]] · [[Colors]]
|
||||
- [[Internationalization & RTL]]
|
||||
- [[Settings & Theming]]
|
||||
Reference in New Issue
Block a user