fix: organize appearance settings sections

Separated theme and localization controls
Added localized section title
This commit is contained in:
Bohdan Triapitsyn
2026-04-26 17:21:52 +03:00
parent 7205d7a34f
commit be723fa58b
6 changed files with 147 additions and 130 deletions
@@ -445,6 +445,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const isVSCode = isVSCodeRuntime();
const hasAppearanceSettings = (shouldShow('theme') || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode;
const hasThemeSettings = shouldShow('theme') && !isVSCode;
const hasLocalizationSettings = (shouldShow('theme') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode;
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile;
const hasBehaviorSettings = shouldShow('mermaidRendering')
@@ -580,10 +582,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{/* --- Appearance & Themes --- */}
{hasAppearanceSettings && (
<div className="mb-8 space-y-3">
<section className="px-2 pb-2 pt-0 space-y-0.5">
<div className="pb-1.5">
<div className="mb-8 space-y-6">
{hasThemeSettings && (
<section className="px-2 pb-2 pt-0 space-y-2">
<div className="flex min-w-0 flex-col gap-1.5">
<span className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.colorMode')}</span>
<div className="flex flex-wrap items-center gap-1">
@@ -593,149 +594,159 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
variant="chip"
size="xs"
aria-pressed={themeMode === option.value}
className="!font-normal"
onClick={() => setThemeMode(option.value)}
>
className="!font-normal"
onClick={() => setThemeMode(option.value)}
>
{tUnsafe(option.labelKey)}
</Button>
))}
</div>
</div>
</div>
<div className="mt-2 grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
<div className="flex min-w-0 flex-col">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.appearance.language.label')}</span>
<span className="typography-meta text-muted-foreground">{t('settings.appearance.language.description')}</span>
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.lightTheme')}</span>
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectLightThemeAria')} className="w-fit">
<SelectValue placeholder={t('settings.openchamber.visual.field.selectThemePlaceholder')}>
{selectedLightTheme
? formatThemeLabel(selectedLightTheme.metadata.name, 'light')
: undefined}
</SelectValue>
</SelectTrigger>
<SelectContent>
{lightThemes.map((theme) => (
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
{formatThemeLabel(theme.metadata.name, 'light')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.darkTheme')}</span>
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectDarkThemeAria')} className="w-fit">
<SelectValue placeholder={t('settings.openchamber.visual.field.selectThemePlaceholder')}>
{selectedDarkTheme
? formatThemeLabel(selectedDarkTheme.metadata.name, 'dark')
: undefined}
</SelectValue>
</SelectTrigger>
<SelectContent>
{darkThemes.map((theme) => (
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
{formatThemeLabel(theme.metadata.name, 'dark')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Select value={locale} onValueChange={(value) => setLocale(value as Locale)}>
<SelectTrigger aria-label={t('settings.appearance.language.select')} className="w-fit">
<SelectValue>{label(locale)}</SelectValue>
</SelectTrigger>
<SelectContent>
{locales.map((availableLocale) => (
<SelectItem key={availableLocale} value={availableLocale}>
{label(availableLocale)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="mt-2 grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.lightTheme')}</span>
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectLightThemeAria')} className="w-fit">
<SelectValue placeholder={t('settings.openchamber.visual.field.selectThemePlaceholder')}>
{selectedLightTheme
? formatThemeLabel(selectedLightTheme.metadata.name, 'light')
: undefined}
</SelectValue>
<div className="flex items-center gap-2 py-1.5">
<button
type="button"
disabled={customThemesLoading || themesReloading}
onClick={() => {
const startedAt = Date.now();
setThemesReloading(true);
void reloadCustomThemes().finally(() => {
const elapsed = Date.now() - startedAt;
if (elapsed < 500) {
window.setTimeout(() => {
setThemesReloading(false);
}, 500 - elapsed);
return;
}
setThemesReloading(false);
});
}}
className="inline-flex items-center typography-ui-label font-normal text-foreground underline decoration-[1px] underline-offset-2 hover:text-foreground/80 disabled:cursor-not-allowed disabled:text-muted-foreground/60"
>
{themesReloading ? t('settings.openchamber.visual.actions.reloadingThemes') : t('settings.openchamber.visual.actions.reloadThemes')}
</button>
<Tooltip delayDuration={700}>
<TooltipTrigger asChild>
<button
type="button"
className="flex items-center justify-center rounded-md p-1 text-muted-foreground/70 hover:text-foreground"
aria-label={t('settings.openchamber.visual.field.themeImportInfoAria')}
>
<RiInformationLine className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent sideOffset={8}>
{t('settings.openchamber.visual.field.themeImportInfoTooltip')}
</TooltipContent>
</Tooltip>
</div>
</section>
)}
{hasLocalizationSettings && (
<section className="px-2 pb-2 pt-0 space-y-2">
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.localization')}</h4>
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
<div className="flex min-w-0 flex-col">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.appearance.language.label')}</span>
<span className="typography-meta text-muted-foreground">{t('settings.appearance.language.description')}</span>
</div>
<Select value={locale} onValueChange={(value) => setLocale(value as Locale)}>
<SelectTrigger aria-label={t('settings.appearance.language.select')} className="w-fit">
<SelectValue>{label(locale)}</SelectValue>
</SelectTrigger>
<SelectContent>
{lightThemes.map((theme) => (
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
{formatThemeLabel(theme.metadata.name, 'light')}
{locales.map((availableLocale) => (
<SelectItem key={availableLocale} value={availableLocale}>
{label(availableLocale)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.darkTheme')}</span>
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectDarkThemeAria')} className="w-fit">
<SelectValue placeholder={t('settings.openchamber.visual.field.selectThemePlaceholder')}>
{selectedDarkTheme
? formatThemeLabel(selectedDarkTheme.metadata.name, 'dark')
: undefined}
</SelectValue>
</SelectTrigger>
<SelectContent>
{darkThemes.map((theme) => (
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
{formatThemeLabel(theme.metadata.name, 'dark')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{(shouldShow('timeFormat') || shouldShow('weekStart')) && (
<div className="mt-1 grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
{shouldShow('timeFormat') && (
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.timeFormat')}</span>
<Select value={timeFormatPreference} onValueChange={(value: 'auto' | '12h' | '24h') => handleTimeFormatPreferenceChange(value)}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectTimeFormatAria')} className="w-fit">
<SelectValue>{selectedTimeFormatLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
{TIME_FORMAT_OPTIONS.map((option) => (
<SelectItem key={option.id} value={option.id}>{tUnsafe(option.labelKey)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{(shouldShow('timeFormat') || shouldShow('weekStart')) && (
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
{shouldShow('timeFormat') && (
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.timeFormat')}</span>
<Select value={timeFormatPreference} onValueChange={(value: 'auto' | '12h' | '24h') => handleTimeFormatPreferenceChange(value)}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectTimeFormatAria')} className="w-fit">
<SelectValue>{selectedTimeFormatLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
{TIME_FORMAT_OPTIONS.map((option) => (
<SelectItem key={option.id} value={option.id}>{tUnsafe(option.labelKey)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{shouldShow('weekStart') && (
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.weekStartsOn')}</span>
<Select value={weekStartPreference} onValueChange={(value: 'auto' | 'monday' | 'sunday') => handleWeekStartPreferenceChange(value)}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectWeekStartAria')} className="w-fit">
<SelectValue>{selectedWeekStartLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
{WEEK_START_OPTIONS.map((option) => (
<SelectItem key={option.id} value={option.id}>{tUnsafe(option.labelKey)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
{shouldShow('weekStart') && (
<div className="flex min-w-0 items-center gap-2">
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.weekStartsOn')}</span>
<Select value={weekStartPreference} onValueChange={(value: 'auto' | 'monday' | 'sunday') => handleWeekStartPreferenceChange(value)}>
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectWeekStartAria')} className="w-fit">
<SelectValue>{selectedWeekStartLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
{WEEK_START_OPTIONS.map((option) => (
<SelectItem key={option.id} value={option.id}>{tUnsafe(option.labelKey)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
</section>
)}
<div className="flex items-center gap-2 py-1.5">
<button
type="button"
disabled={customThemesLoading || themesReloading}
onClick={() => {
const startedAt = Date.now();
setThemesReloading(true);
void reloadCustomThemes().finally(() => {
const elapsed = Date.now() - startedAt;
if (elapsed < 500) {
window.setTimeout(() => {
setThemesReloading(false);
}, 500 - elapsed);
return;
}
setThemesReloading(false);
});
}}
className="inline-flex items-center typography-ui-label font-normal text-foreground underline decoration-[1px] underline-offset-2 hover:text-foreground/80 disabled:cursor-not-allowed disabled:text-muted-foreground/60"
>
{themesReloading ? t('settings.openchamber.visual.actions.reloadingThemes') : t('settings.openchamber.visual.actions.reloadThemes')}
</button>
<Tooltip delayDuration={700}>
<TooltipTrigger asChild>
<button
type="button"
className="flex items-center justify-center rounded-md p-1 text-muted-foreground/70 hover:text-foreground"
aria-label={t('settings.openchamber.visual.field.themeImportInfoAria')}
>
<RiInformationLine className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent sideOffset={8}>
{t('settings.openchamber.visual.field.themeImportInfoTooltip')}
</TooltipContent>
</Tooltip>
</div>
{(showPwaInstallNameSetting || showPwaOrientationSetting) && (
<section className="px-2 pb-2 pt-0 space-y-2">
{showPwaInstallNameSetting && (
<div className="py-1.5 space-y-1.5">
@@ -824,7 +835,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</div>
</div>
)}
</section>
</section>
)}
</div>
)}
@@ -1322,6 +1322,7 @@ export const settingsDict = {
'settings.voice.page.preview.voiceLine': 'Hello! I\'m {voiceName}. This is how I sound.',
'settings.voice.page.preview.customServerLine': 'Hello! This is a preview of the custom TTS server.',
'settings.openchamber.visual.section.colorMode': 'Color Mode',
'settings.openchamber.visual.section.localization': 'Localization',
'settings.openchamber.visual.section.spacingAndLayout': 'Spacing & Layout',
'settings.openchamber.visual.section.navigation': 'Navigation',
'settings.openchamber.visual.section.chatRenderMode': 'Chat Render Mode',
@@ -1322,6 +1322,7 @@ export const settingsDict = {
"settings.voice.page.preview.voiceLine": "¡Hola! Soy {voiceName}. Así suena mi voz.",
"settings.voice.page.preview.customServerLine": "¡Hola! Esta es una previsualización del servidor TTS personalizado.",
"settings.openchamber.visual.section.colorMode": "Modo de color",
"settings.openchamber.visual.section.localization": "Localización",
"settings.openchamber.visual.section.spacingAndLayout": "Espaciado y diseño",
"settings.openchamber.visual.section.navigation": "Navegación",
"settings.openchamber.visual.section.chatRenderMode": "Modo de renderizado de chat",
@@ -1322,6 +1322,7 @@ export const settingsDict = {
"settings.voice.page.preview.voiceLine": "Olá! Sou {voiceName}. É assim que minha voz soa.",
"settings.voice.page.preview.customServerLine": "Olá! Esta é uma prévia do servidor TTS personalizado.",
"settings.openchamber.visual.section.colorMode": "Modo de cor",
"settings.openchamber.visual.section.localization": "Localização",
"settings.openchamber.visual.section.spacingAndLayout": "Espaçamento e layout",
"settings.openchamber.visual.section.navigation": "Navegação",
"settings.openchamber.visual.section.chatRenderMode": "Modo de renderizado de chat",
@@ -1322,6 +1322,7 @@ export const settingsDict = {
"settings.voice.page.preview.voiceLine": "Привіт! Я {voiceName}. Ось як я звучу.",
"settings.voice.page.preview.customServerLine": "Привіт! Це попередній перегляд спеціального сервера TTS.",
"settings.openchamber.visual.section.colorMode": "Режим теми",
"settings.openchamber.visual.section.localization": "Локалізація",
"settings.openchamber.visual.section.spacingAndLayout": "Відступи й компонування",
"settings.openchamber.visual.section.navigation": "Навігація",
"settings.openchamber.visual.section.chatRenderMode": "Режим візуалізації чату",
@@ -1322,6 +1322,7 @@ export const settingsDict = {
'settings.voice.page.preview.voiceLine': '你好!我是 {voiceName}。这是我的声音效果。',
'settings.voice.page.preview.customServerLine': '你好!这是自定义 TTS 服务器的预览。',
'settings.openchamber.visual.section.colorMode': '颜色模式',
'settings.openchamber.visual.section.localization': '本地化',
'settings.openchamber.visual.section.spacingAndLayout': '间距与布局',
'settings.openchamber.visual.section.navigation': '导航',
'settings.openchamber.visual.section.chatRenderMode': '聊天渲染模式',