Files
nick-doc/05 - Design System/Settings & Theming.md
Siavash Sameni dceaf82934 audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs:
- Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md
- 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer).
- Scanner docs from scratch (was zero): architecture, data model, API ref, payment
  flow, operations runbook + repo README.
- Doc-sync updates across API reference, data models, flows, design system.
- Secret Rotation Runbook (08 - Operations) for the exposed credentials.
- Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js.

Issues remain status:open intentionally — the code fixes are uncommitted-then-committed
working-tree changes per repo and aren't "resolved" until merged/deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:48:04 +04:00

223 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 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** | 024 | 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