---
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** | `amaneh` (warm-earth) — multi-swatch picker removed in v2.7.0 | `amaneh` | 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
