Simplify theme settings to a single dropdown with System, Light, and Dark options.

This commit is contained in:
v.lagerev 2026-04-25 13:36:40 +03:00
parent 806476001f
commit 00935aecff
7 changed files with 62 additions and 372 deletions

View file

@ -92,12 +92,10 @@
"general_title": "General", "general_title": "General",
"appearance": "Appearance", "appearance": "Appearance",
"system_theme": "System Theme", "system_theme": "System",
"system_theme_desc": "Choose between light and dark theme based on system preference.", "theme_light": "Light",
"light_theme": "Light Theme:", "theme_dark": "Dark",
"dark_theme": "Dark Theme:",
"theme": "Theme", "theme": "Theme",
"theme_desc": "Theme to use when system theme is not enabled.",
"monochrome_mode": "Monochrome Mode", "monochrome_mode": "Monochrome Mode",
"twitter_emoji": "Twitter Emoji", "twitter_emoji": "Twitter Emoji",
"page_zoom": "Page Zoom", "page_zoom": "Page Zoom",

View file

@ -92,12 +92,10 @@
"general_title": "Общие", "general_title": "Общие",
"appearance": "Внешний вид", "appearance": "Внешний вид",
"system_theme": "Системная тема", "system_theme": "Системная",
"system_theme_desc": "Выбор между светлой и тёмной темой на основе системных настроек.", "theme_light": "Светлая",
"light_theme": "Светлая тема:", "theme_dark": "Тёмная",
"dark_theme": "Тёмная тема:",
"theme": "Тема", "theme": "Тема",
"theme_desc": "Тема при отключённой системной теме.",
"monochrome_mode": "Монохромный режим", "monochrome_mode": "Монохромный режим",
"twitter_emoji": "Эмодзи Twitter", "twitter_emoji": "Эмодзи Twitter",
"page_zoom": "Масштаб страницы", "page_zoom": "Масштаб страницы",

View file

@ -11,7 +11,6 @@ import {
as, as,
Box, Box,
Button, Button,
Chip,
config, config,
Header, Header,
Icon, Icon,
@ -40,10 +39,6 @@ import { isMacOS } from '../../../utils/user-agent';
import { import {
DarkTheme, DarkTheme,
LightTheme, LightTheme,
Theme,
ThemeKind,
useSystemThemeKind,
useThemeNames,
useThemes, useThemes,
} from '../../../hooks/useTheme'; } from '../../../hooks/useTheme';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
@ -52,48 +47,42 @@ import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
import { useDateFormatItems } from '../../../hooks/useDateFormat'; import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
type ThemeSelectorProps = { const SYSTEM_THEME_ID = 'system';
themeNames: Record<string, string>;
themes: Theme[];
selected: Theme;
onSelect: (theme: Theme) => void;
};
const ThemeSelector = as<'div', ThemeSelectorProps>(
({ themeNames, themes, selected, onSelect, ...props }, ref) => (
<Menu {...props} ref={ref}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
{themes.map((theme) => (
<MenuItem
key={theme.id}
size="300"
variant={theme.id === selected.id ? 'Primary' : 'Surface'}
radii="300"
onClick={() => onSelect(theme)}
>
<Text size="T300">{themeNames[theme.id] ?? theme.id}</Text>
</MenuItem>
))}
</Box>
</Menu>
)
);
function SelectTheme({ disabled }: { disabled?: boolean }) { const THEME_I18N_KEYS: Record<string, string> = {
[LightTheme.id]: 'Settings.theme_light',
[DarkTheme.id]: 'Settings.theme_dark',
};
function ThemeSelect() {
const { t } = useTranslation();
const themes = useThemes(); const themes = useThemes();
const themeNames = useThemeNames(); const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId'); const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
const [menuCords, setMenuCords] = useState<RectCords>(); const [menuCords, setMenuCords] = useState<RectCords>();
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
const handleThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => { const themeName = (id: string) => t(THEME_I18N_KEYS[id] ?? id);
const currentLabel = systemTheme
? t('Settings.system_theme')
: themeName(themeId ?? LightTheme.id);
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(evt.currentTarget.getBoundingClientRect()); setMenuCords(evt.currentTarget.getBoundingClientRect());
}; };
const handleThemeSelect = (theme: Theme) => { const handleSelect = (id: string) => {
setThemeId(theme.id); if (id === SYSTEM_THEME_ID) {
setSystemTheme(true);
} else {
setSystemTheme(false);
setThemeId(id);
}
setMenuCords(undefined); setMenuCords(undefined);
}; };
const selectedId = systemTheme ? SYSTEM_THEME_ID : (themeId ?? LightTheme.id);
return ( return (
<> <>
<Button <Button
@ -103,10 +92,9 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
fill="Soft" fill="Soft"
radii="300" radii="300"
after={<Icon size="300" src={Icons.ChevronBottom} />} after={<Icon size="300" src={Icons.ChevronBottom} />}
onClick={disabled ? undefined : handleThemeMenu} onClick={handleOpenMenu}
aria-disabled={disabled}
> >
<Text size="T300">{themeNames[selectedTheme.id] ?? selectedTheme.id}</Text> <Text size="T300">{currentLabel}</Text>
</Button> </Button>
<PopOut <PopOut
anchor={menuCords} anchor={menuCords}
@ -126,12 +114,29 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<ThemeSelector <Menu>
themeNames={themeNames} <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
themes={themes} <MenuItem
selected={selectedTheme} size="300"
onSelect={handleThemeSelect} variant={selectedId === SYSTEM_THEME_ID ? 'Primary' : 'Surface'}
/> radii="300"
onClick={() => handleSelect(SYSTEM_THEME_ID)}
>
<Text size="T300">{t('Settings.system_theme')}</Text>
</MenuItem>
{themes.map((theme) => (
<MenuItem
key={theme.id}
size="300"
variant={selectedId === theme.id ? 'Primary' : 'Surface'}
radii="300"
onClick={() => handleSelect(theme.id)}
>
<Text size="T300">{themeName(theme.id)}</Text>
</MenuItem>
))}
</Box>
</Menu>
</FocusTrap> </FocusTrap>
} }
/> />
@ -139,128 +144,6 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
); );
} }
function SystemThemePreferences() {
const { t } = useTranslation();
const themeKind = useSystemThemeKind();
const themeNames = useThemeNames();
const themes = useThemes();
const [lightThemeId, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId');
const lightThemes = themes.filter((theme) => theme.kind === ThemeKind.Light);
const darkThemes = themes.filter((theme) => theme.kind === ThemeKind.Dark);
const selectedLightTheme = lightThemes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
const selectedDarkTheme = darkThemes.find((theme) => theme.id === darkThemeId) ?? DarkTheme;
const [ltCords, setLTCords] = useState<RectCords>();
const [dtCords, setDTCords] = useState<RectCords>();
const handleLightThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setLTCords(evt.currentTarget.getBoundingClientRect());
};
const handleDarkThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setDTCords(evt.currentTarget.getBoundingClientRect());
};
const handleLightThemeSelect = (theme: Theme) => {
setLightThemeId(theme.id);
setLTCords(undefined);
};
const handleDarkThemeSelect = (theme: Theme) => {
setDarkThemeId(theme.id);
setDTCords(undefined);
};
return (
<Box wrap="Wrap" gap="400">
<SettingTile
title={t('Settings.light_theme')}
after={
<Chip
variant={themeKind === ThemeKind.Light ? 'Primary' : 'Secondary'}
outlined={themeKind === ThemeKind.Light}
radii="Pill"
after={<Icon size="200" src={Icons.ChevronBottom} />}
onClick={handleLightThemeMenu}
>
<Text size="B300">{themeNames[selectedLightTheme.id] ?? selectedLightTheme.id}</Text>
</Chip>
}
/>
<PopOut
anchor={ltCords}
offset={5}
position="Bottom"
align="End"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setLTCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
isKeyBackward: (evt: KeyboardEvent) =>
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
escapeDeactivates: stopPropagation,
}}
>
<ThemeSelector
themeNames={themeNames}
themes={lightThemes}
selected={selectedLightTheme}
onSelect={handleLightThemeSelect}
/>
</FocusTrap>
}
/>
<SettingTile
title={t('Settings.dark_theme')}
after={
<Chip
variant={themeKind === ThemeKind.Dark ? 'Primary' : 'Secondary'}
outlined={themeKind === ThemeKind.Dark}
radii="Pill"
after={<Icon size="200" src={Icons.ChevronBottom} />}
onClick={handleDarkThemeMenu}
>
<Text size="B300">{themeNames[selectedDarkTheme.id] ?? selectedDarkTheme.id}</Text>
</Chip>
}
/>
<PopOut
anchor={dtCords}
offset={5}
position="Bottom"
align="End"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setDTCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
isKeyBackward: (evt: KeyboardEvent) =>
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
escapeDeactivates: stopPropagation,
}}
>
<ThemeSelector
themeNames={themeNames}
themes={darkThemes}
selected={selectedDarkTheme}
onSelect={handleDarkThemeSelect}
/>
</FocusTrap>
}
/>
</Box>
);
}
function PageZoomInput() { function PageZoomInput() {
const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom'); const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`); const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
@ -307,32 +190,16 @@ function PageZoomInput() {
function Appearance() { function Appearance() {
const { t } = useTranslation(); const { t } = useTranslation();
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode'); const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
return ( return (
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">{t('Settings.appearance')}</Text> <Text size="L400">{t('Settings.appearance')}</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title={t('Settings.system_theme')}
description={t('Settings.system_theme_desc')}
after={<Switch variant="Primary" value={systemTheme} onChange={setSystemTheme} />}
/>
{systemTheme && <SystemThemePreferences />}
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile
title={t('Settings.theme')} title={t('Settings.theme')}
description={t('Settings.theme_desc')} after={<ThemeSelect />}
after={<SelectTheme disabled={systemTheme} />}
/> />
</SequenceCard> </SequenceCard>

View file

@ -1,7 +1,7 @@
import { lightTheme } from 'folds'; import { lightTheme } from 'folds';
import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { onDarkFontWeight, onLightFontWeight } from '../../config.css'; import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
import { butterTheme, darkTheme, silverTheme } from '../../colors.css'; import { darkTheme } from '../../colors.css';
import { settingsAtom } from '../state/settings'; import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings'; import { useSetting } from '../state/hooks/settings';
@ -22,39 +22,18 @@ export const LightTheme: Theme = {
classNames: [lightTheme, onLightFontWeight, 'prism-light'], classNames: [lightTheme, onLightFontWeight, 'prism-light'],
}; };
export const SilverTheme: Theme = {
id: 'silver-theme',
kind: ThemeKind.Light,
classNames: ['silver-theme', silverTheme, onLightFontWeight, 'prism-light'],
};
export const DarkTheme: Theme = { export const DarkTheme: Theme = {
id: 'dark-theme', id: 'dark-theme',
kind: ThemeKind.Dark, kind: ThemeKind.Dark,
classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'], classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'],
}; };
export const ButterTheme: Theme = {
id: 'butter-theme',
kind: ThemeKind.Dark,
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
};
export const useThemes = (): Theme[] => { export const useThemes = (): Theme[] => {
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []); const themes: Theme[] = useMemo(() => [LightTheme, DarkTheme], []);
return themes; return themes;
}; };
export const useThemeNames = (): Record<string, string> =>
useMemo(
() => ({
[LightTheme.id]: 'Light',
[SilverTheme.id]: 'Silver',
[DarkTheme.id]: 'Dark',
[ButterTheme.id]: 'Butter',
}),
[]
);
export const useSystemThemeKind = (): ThemeKind => { export const useSystemThemeKind = (): ThemeKind => {
const darkModeQueryList = useMemo(() => window.matchMedia('(prefers-color-scheme: dark)'), []); const darkModeQueryList = useMemo(() => window.matchMedia('(prefers-color-scheme: dark)'), []);
const [themeKind, setThemeKind] = useState<ThemeKind>( const [themeKind, setThemeKind] = useState<ThemeKind>(
@ -77,24 +56,14 @@ export const useSystemThemeKind = (): ThemeKind => {
export const useActiveTheme = (): Theme => { export const useActiveTheme = (): Theme => {
const systemThemeKind = useSystemThemeKind(); const systemThemeKind = useSystemThemeKind();
const themes = useThemes();
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme'); const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [themeId] = useSetting(settingsAtom, 'themeId'); const [themeId] = useSetting(settingsAtom, 'themeId');
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
if (!systemTheme) { if (!systemTheme) {
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme; return themeId === DarkTheme.id ? DarkTheme : LightTheme;
return selectedTheme;
} }
const selectedTheme = return systemThemeKind === ThemeKind.Dark ? DarkTheme : LightTheme;
systemThemeKind === ThemeKind.Dark
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
return selectedTheme;
}; };
const ThemeContext = createContext<Theme | null>(null); const ThemeContext = createContext<Theme | null>(null);

View file

@ -18,8 +18,6 @@ export enum MessageLayout {
export interface Settings { export interface Settings {
themeId?: string; themeId?: string;
useSystemTheme: boolean; useSystemTheme: boolean;
lightThemeId?: string;
darkThemeId?: string;
monochromeMode?: boolean; monochromeMode?: boolean;
isMarkdown: boolean; isMarkdown: boolean;
editorToolbar: boolean; editorToolbar: boolean;
@ -51,8 +49,6 @@ export interface Settings {
const defaultSettings: Settings = { const defaultSettings: Settings = {
themeId: undefined, themeId: undefined,
useSystemTheme: true, useSystemTheme: true,
lightThemeId: undefined,
darkThemeId: undefined,
monochromeMode: false, monochromeMode: false,
isMarkdown: true, isMarkdown: true,
editorToolbar: false, editorToolbar: false,

View file

@ -5,103 +5,6 @@ import { color } from 'folds';
const navDark = '#121314'; // левая панель (навигация) const navDark = '#121314'; // левая панель (навигация)
const contentDark = '#0d0d0e'; // правая часть (контент/чат) const contentDark = '#0d0d0e'; // правая часть (контент/чат)
export const silverTheme = createTheme(color, {
Background: {
Container: '#DEDEDE',
ContainerHover: '#D3D3D3',
ContainerActive: '#C7C7C7',
ContainerLine: '#BBBBBB',
OnContainer: '#000000',
},
Surface: {
Container: '#EAEAEA',
ContainerHover: '#DEDEDE',
ContainerActive: '#D3D3D3',
ContainerLine: '#C7C7C7',
OnContainer: '#000000',
},
SurfaceVariant: {
Container: '#DEDEDE',
ContainerHover: '#D3D3D3',
ContainerActive: '#C7C7C7',
ContainerLine: '#BBBBBB',
OnContainer: '#000000',
},
Primary: {
Main: '#1245A8',
MainHover: '#103E97',
MainActive: '#0F3B8F',
MainLine: '#0E3786',
OnMain: '#FFFFFF',
Container: '#C4D0E9',
ContainerHover: '#B8C7E5',
ContainerActive: '#ACBEE1',
ContainerLine: '#A0B5DC',
OnContainer: '#0D3076',
},
Secondary: {
Main: '#000000',
MainHover: '#171717',
MainActive: '#232323',
MainLine: '#2F2F2F',
OnMain: '#EAEAEA',
Container: '#C7C7C7',
ContainerHover: '#BBBBBB',
ContainerActive: '#AFAFAF',
ContainerLine: '#A4A4A4',
OnContainer: '#0C0C0C',
},
Success: {
Main: '#017343',
MainHover: '#01683C',
MainActive: '#016239',
MainLine: '#015C36',
OnMain: '#FFFFFF',
Container: '#BFDCD0',
ContainerHover: '#B3D5C7',
ContainerActive: '#A6CEBD',
ContainerLine: '#99C7B4',
OnContainer: '#01512F',
},
Warning: {
Main: '#864300',
MainHover: '#793C00',
MainActive: '#723900',
MainLine: '#6B3600',
OnMain: '#FFFFFF',
Container: '#E1D0BF',
ContainerHover: '#DBC7B2',
ContainerActive: '#D5BDA6',
ContainerLine: '#CFB499',
OnContainer: '#5E2F00',
},
Critical: {
Main: '#9D0F0F',
MainHover: '#8D0E0E',
MainActive: '#850D0D',
MainLine: '#7E0C0C',
OnMain: '#FFFFFF',
Container: '#E7C3C3',
ContainerHover: '#E2B7B7',
ContainerActive: '#DDABAB',
ContainerLine: '#D89F9F',
OnContainer: '#6E0B0B',
},
Other: {
FocusRing: 'rgba(0 0 0 / 50%)',
Shadow: 'rgba(0 0 0 / 20%)',
Overlay: 'rgba(0 0 0 / 50%)',
},
});
const darkThemeData = { const darkThemeData = {
Background: { Background: {
Container: navDark, Container: navDark,
@ -200,43 +103,3 @@ const darkThemeData = {
}; };
export const darkTheme = createTheme(color, darkThemeData); export const darkTheme = createTheme(color, darkThemeData);
export const butterTheme = createTheme(color, {
...darkThemeData,
Background: {
Container: '#1A1916',
ContainerHover: '#262621',
ContainerActive: '#33322C',
ContainerLine: '#403F38',
OnContainer: '#FFFBDE',
},
Surface: {
Container: '#262621',
ContainerHover: '#33322C',
ContainerActive: '#403F38',
ContainerLine: '#4D4B43',
OnContainer: '#FFFBDE',
},
SurfaceVariant: {
Container: '#33322C',
ContainerHover: '#403F38',
ContainerActive: '#4D4B43',
ContainerLine: '#59584E',
OnContainer: '#FFFBDE',
},
Secondary: {
Main: '#FFFBDE',
MainHover: '#E5E2C8',
MainActive: '#D9D5BD',
MainLine: '#CCC9B2',
OnMain: '#1A1916',
Container: '#403F38',
ContainerHover: '#4D4B43',
ContainerActive: '#59584E',
ContainerLine: '#666459',
OnContainer: '#F2EED3',
},
});

View file

@ -22,8 +22,7 @@
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif; --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
} }
.dark-theme, .dark-theme {
.butter-theme {
--tc-link: hsl(213deg 100% 80%); --tc-link: hsl(213deg 100% 80%);
--mx-uc-1: hsl(208, 100%, 75%); --mx-uc-1: hsl(208, 100%, 75%);