Initial commit: nick docs
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user