fix: organize appearance settings sections
Separated theme and localization controls Added localized section title
This commit is contained in:
@@ -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': '聊天渲染模式',
|
||||
|
||||
Reference in New Issue
Block a user