76 lines
3.3 KiB
TypeScript
76 lines
3.3 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={{ '& .MuiToggleButton-root': { gap: 0.75, paddingBlock: 0.75, 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;
|