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>
223 lines
6.8 KiB
Markdown
223 lines
6.8 KiB
Markdown
---
|
||
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
|