Files
baya-monorepo/client/src/components/settings/ThemeModeSetting.tsx
T
2026-07-27 23:58:16 +03:30

91 lines
3.9 KiB
TypeScript

'use client';
/*
* The ONLY React subtree subscribed to useColorScheme(). When the user picks a mode:
* 1. setMode(...) writes MUI's mode
* 2. the resolved scheme lands on <html data-mui-color-scheme>
* 3. CSS custom properties resolve to new values → browser repaints
* 4. React re-renders ONLY this component (which segment reads as selected)
* Nothing above it in the tree is touched.
*/
import { FunctionComponent } from 'react';
import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
import { useColorScheme } from '@mui/material/styles';
import { useTranslations } from 'next-intl';
import AppIcon from '@/components/common/AppIcon';
type ThemeMode = 'light' | 'dark' | 'system';
const MODES: ReadonlyArray<{ mode: ThemeMode; icon: string; key: string }> = [
{ mode: 'light', icon: 'light_mode', key: 'theme_light' },
{ mode: 'dark', icon: 'dark_mode', key: 'theme_dark' },
{ mode: 'system', icon: 'system_mode', key: 'theme_system' },
];
/**
* The app's one appearance control: a three-way segmented choice — روشن / تیره / سیستم.
*
* It replaced an on/off `Switch`, which was wrong twice over. A switch is a **boolean**, so it
* could not express "follow my phone" — yet `system` is the app's actual default (`ThemeProvider`
* `defaultMode`), which left the control silently lying about the current state on a first visit.
* And a lone switch beside a label reads like a feature being enabled rather than a look being
* chosen. Three labelled segments say what each option does and which one is live.
*
* This is the one place `mode` is the right source rather than `colorScheme`: `mode` is the user's
* **choice** (and can be `'system'`), `colorScheme` is only the resolved result. The `?? 'system'`
* fallback is hydration-safe — MUI reports `mode` as `undefined` until it mounts, and the server,
* the first client render, and the pre-mount state all agree on the same default.
* @component ThemeModeSetting
*/
const ThemeModeSetting: FunctionComponent = () => {
const { mode, setMode } = useColorScheme();
const t = useTranslations('common');
return (
<Stack sx={{ gap: 1.25, px: 2, py: 1.5 }}>
<Stack direction="row" sx={{ alignItems: 'center', gap: 1.5 }}>
<AppIcon icon="appearance" size={18} color="var(--bal-text-secondary)" aria-hidden="true" />
<Typography variant="body2">{t('appearance')}</Typography>
</Stack>
<ToggleButtonGroup
exclusive
fullWidth
size="small"
value={mode ?? 'system'}
onChange={(_event, next: ThemeMode | null) => {
// `exclusive` emits null when the active segment is tapped again — keep the choice.
if (next) setMode(next);
}}
aria-label={t('appearance')}
sx={{
gap: 1,
// Each segment is already individually rounded, so MUI's default treatment — segments
// butted edge to edge with the shared border collapsed via a negative inline margin — put
// two rounded corners flush against each other with no separation at all. Spacing them
// makes each one read as its own choice; the negative margin has to be undone explicitly,
// otherwise the gap is eaten by it.
'& .MuiToggleButtonGroup-grouped': {
marginInlineStart: 0,
gap: 0.75,
paddingBlock: 0.75,
border: '1px solid',
borderColor: 'divider',
borderRadius: 'var(--bal-radius-sm)',
},
}}
>
{MODES.map((option) => (
<ToggleButton key={option.mode} value={option.mode}>
<AppIcon icon={option.icon} size={16} aria-hidden="true" />
<Typography variant="caption" sx={{ fontWeight: 500 }}>
{t(option.key)}
</Typography>
</ToggleButton>
))}
</ToggleButtonGroup>
</Stack>
);
};
export default ThemeModeSetting;