Files
webmail/components/settings/accessibility-settings.tsx

628 lines
32 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { useSettingsStore } from '@/stores/settings-store';
import { usePolicyStore } from '@/stores/policy-store';
import { SettingsSection, SettingItem, RadioGroup, Select, ToggleSwitch } from './settings-section';
import { Button } from '@/components/ui/button';
import { ChevronRight, Keyboard, Volume2 } from '@/components/icons';
import { IS_LITE } from '@/lib/lite';
import { useNoThirdPartyTraffic } from '@/hooks/use-third-party-mode';
import { openKeyboardShortcuts } from '@/lib/app-commands';
import { voicesForLocale } from '@/lib/read-aloud';
import { useSpeechVoices } from '@/hooks/use-read-aloud';
import { NOTIFICATION_SOUNDS, playNotificationSound, type NotificationSoundChoice } from '@/lib/notification-sound';
import {
ACCESSIBILITY_DEFAULTS,
ACCESSIBILITY_KEYS,
FONT_SIZES,
MAIL_MIN_FONT_SIZES,
MAIL_TEXT_SCALES,
READ_ALOUD_RATES,
TOAST_DURATIONS,
toastDurationAtLeast,
type FontSize,
} from '@/lib/accessibility-settings';
import {
ACCESSIBILITY_PRESET_IDS,
PRESET_SETTING_LABELS,
presetChanges,
type AccessibilityPresetId,
type PresetChange,
} from '@/lib/accessibility-presets';
import { AccessibilityPresetPreview } from './accessibility-preset-preview';
import { FONT_SIZES as COMPOSE_FONT_SIZES } from '@/components/email/font-size';
import type { Density, SendDelaySeconds } from '@/stores/settings-store';
type SettingKey = Parameters<ReturnType<typeof useSettingsStore.getState>['updateSetting']>[0];
/** Opens another settings page (and section) the way deep links do. */
function goToSettings(tab: string, section?: string) {
window.dispatchEvent(new CustomEvent('settings-tab-change', { detail: section ? { tab, section } : tab }));
}
function LinkButton({ label, onClick }: { label: string; onClick: () => void }) {
return (
<Button variant="outline" size="sm" onClick={onClick}>
{label}
<ChevronRight className="w-4 h-4 ms-1" />
</Button>
);
}
/**
* Settings > Accessibility: presets on top, then the options by topic. Rows
* that are also everyday preferences (conversation view, undo send, sounds…)
* are the same setting shown on their own page too, not a copy.
*/
export function AccessibilitySettings() {
const t = useTranslations('settings.accessibility');
const tRoot = useTranslations();
const tEmail = useTranslations('settings.email_behavior');
const tAppearance = useTranslations('settings.appearance');
const tNotifications = useTranslations('settings.notifications');
const tCalendar = useTranslations('calendar.settings');
const settings = useSettingsStore();
const { updateSetting } = settings;
const { isSettingLocked, isSettingHidden, getRestriction } = usePolicyStore();
const faviconsAllowed = !useNoThirdPartyTraffic();
const [applied, setApplied] = useState<{ id: AccessibilityPresetId; changes: PresetChange[] } | null>(null);
const [resetDone, setResetDone] = useState(false);
const locale = useLocale();
const speechVoices = useSpeechVoices();
const toastMin = getRestriction('toastDuration')?.min;
const allowsValue = (key: string, value: unknown) =>
key !== 'toastDuration' || typeof toastMin !== 'number' || toastDurationAtLeast(value as number, toastMin);
const set = (key: string, value: unknown) => updateSetting(key as SettingKey, value as never);
const applyPreset = (id: AccessibilityPresetId) => {
const changes = presetChanges(id, useSettingsStore.getState() as unknown as Record<string, unknown>, isSettingLocked, allowsValue);
for (const change of changes) set(change.key, change.to);
setApplied({ id, changes });
setResetDone(false);
};
const undoPreset = () => {
if (!applied) return;
for (const change of applied.changes) set(change.key, change.from);
setApplied(null);
};
const resetAll = () => {
for (const key of ACCESSIBILITY_KEYS) {
if (isSettingLocked(key)) continue;
set(key, ACCESSIBILITY_DEFAULTS[key]);
}
set('fontSize', 'medium');
setApplied(null);
setResetDone(true);
};
const systemToggle = (key: 'reduceMotion' | 'reduceTransparency' | 'pauseAnimatedImages' | 'largeTargets') => (
<RadioGroup
value={settings[key]}
onChange={(value) => set(key, value)}
options={[
{ value: 'system', label: t('system') },
{ value: 'on', label: t('on') },
{ value: 'off', label: t('off') },
]}
/>
);
// Voices for the interface language, plus the one already chosen even if
// this browser doesn't have it (another device may).
const localeVoices = voicesForLocale(speechVoices, locale);
const readAloudVoiceOptions = [
{ value: '', label: t('read_aloud_voice.default') },
...localeVoices.map((voice) => ({ value: voice.voiceURI, label: voice.name })),
...(settings.readAloudVoice && !localeVoices.some((voice) => voice.voiceURI === settings.readAloudVoice)
? [{ value: settings.readAloudVoice, label: speechVoices.find((voice) => voice.voiceURI === settings.readAloudVoice)?.name ?? settings.readAloudVoice }]
: []),
];
const toastOptions = TOAST_DURATIONS
.filter((value) => allowsValue('toastDuration', value))
.map((value) => ({
value: String(value),
label: value === 0 ? t('toast_duration.until_closed') : t('toast_duration.seconds', { seconds: value }),
}));
return (
<div className="space-y-8">
<SettingsSection id="presets" title={t('presets.title')} description={t('presets.description')}>
<div className="grid gap-2 sm:grid-cols-2" role="group" aria-label={t('presets.title')}>
{ACCESSIBILITY_PRESET_IDS.map((id) => (
<button
key={id}
type="button"
onClick={() => applyPreset(id)}
className="flex items-start gap-3 text-start rounded-md border border-border px-3 py-2.5 hover:bg-muted transition-colors"
>
<AccessibilityPresetPreview id={id} />
<span className="min-w-0">
<span className="block text-sm font-medium text-foreground">{t(`presets.${id}.label`)}</span>
<span className="block text-xs text-muted-foreground mt-0.5">{t(`presets.${id}.description`)}</span>
</span>
</button>
))}
</div>
<div role="status" aria-live="polite">
{applied && (
<div className="rounded-md border border-border bg-muted/40 px-3 py-2.5 space-y-2" data-testid="preset-summary">
<p className="text-sm font-medium text-foreground">
{applied.changes.length === 0
? t('presets.nothing_changed', { name: t(`presets.${applied.id}.label`) })
: t('presets.applied', { name: t(`presets.${applied.id}.label`), count: applied.changes.length })}
</p>
{applied.changes.length > 0 && (
<ul className="text-xs text-muted-foreground list-disc ps-5 space-y-0.5">
{applied.changes.map((change) => (
<li key={change.key}>{PRESET_SETTING_LABELS[change.key] ? tRoot(PRESET_SETTING_LABELS[change.key]) : change.key}</li>
))}
</ul>
)}
<div className="flex flex-wrap gap-2">
{applied.changes.length > 0 && (
<Button variant="outline" size="sm" onClick={undoPreset}>{t('presets.undo')}</Button>
)}
{!settings.showAccessibilityButton && (
<Button variant="outline" size="sm" onClick={() => set('showAccessibilityButton', true)}>
{t('presets.show_button')}
</Button>
)}
</div>
</div>
)}
</div>
<SettingItem label={t('quick_button.label')} description={t('quick_button.description')}>
<ToggleSwitch checked={settings.showAccessibilityButton} onChange={(checked) => set('showAccessibilityButton', checked)} />
</SettingItem>
</SettingsSection>
<SettingsSection id="text" title={t('sections.text')}>
<SettingItem label={t('text_size.label')} description={t('text_size.description')}>
<Select
value={settings.fontSize}
onChange={(value) => set('fontSize', value as FontSize)}
options={FONT_SIZES.map((size) => ({ value: size, label: t(`text_size.${size}`) }))}
/>
</SettingItem>
<SettingItem label={t('font.label')} description={t('font.description')}>
<Select
value={settings.appFont}
onChange={(value) => set('appFont', value)}
options={[
{ value: 'default', label: t('font.default') },
{ value: 'atkinson', label: 'Atkinson Hyperlegible' },
{ value: 'lexend', label: 'Lexend' },
{ value: 'opendyslexic', label: 'OpenDyslexic' },
]}
/>
</SettingItem>
<SettingItem label={t('plain_text_font.label')} description={t('plain_text_font.description')}>
<Select
value={settings.plainTextFont}
onChange={(value) => set('plainTextFont', value)}
options={[
{ value: 'sans', label: t('plain_text_font.sans') },
{ value: 'mono', label: t('plain_text_font.mono') },
]}
/>
</SettingItem>
<SettingItem label={t('line_spacing.label')} description={t('line_spacing.description')}>
<RadioGroup
value={settings.lineSpacing}
onChange={(value) => set('lineSpacing', value)}
options={[
{ value: 'normal', label: t('spacing.normal') },
{ value: 'relaxed', label: t('spacing.relaxed') },
{ value: 'loose', label: t('spacing.loose') },
]}
/>
</SettingItem>
<SettingItem label={t('letter_spacing.label')} description={t('letter_spacing.description')}>
<RadioGroup
value={settings.letterSpacing}
onChange={(value) => set('letterSpacing', value)}
options={[
{ value: 'normal', label: t('spacing.normal') },
{ value: 'wide', label: t('spacing.wide') },
{ value: 'wider', label: t('spacing.wider') },
]}
/>
</SettingItem>
<SettingItem label={t('word_spacing.label')} description={t('word_spacing.description')}>
<RadioGroup
value={settings.wordSpacing}
onChange={(value) => set('wordSpacing', value)}
options={[
{ value: 'normal', label: t('spacing.normal') },
{ value: 'wide', label: t('spacing.wide') },
]}
/>
</SettingItem>
<SettingItem label={t('paragraph_spacing.label')} description={t('paragraph_spacing.description')}>
<RadioGroup
value={settings.paragraphSpacing}
onChange={(value) => set('paragraphSpacing', value)}
options={[
{ value: 'normal', label: t('spacing.normal') },
{ value: 'wide', label: t('spacing.wide') },
]}
/>
</SettingItem>
<SettingItem label={t('reading_width.label')} description={t('reading_width.description')}>
<RadioGroup
value={settings.readingWidth}
onChange={(value) => set('readingWidth', value)}
options={[
{ value: 'full', label: t('reading_width.full') },
{ value: 'comfortable', label: t('reading_width.comfortable') },
]}
/>
</SettingItem>
<SettingItem label={t('reading_mask.label')} description={t('reading_mask.description')}>
<ToggleSwitch checked={settings.readingMask} onChange={(checked) => set('readingMask', checked)} />
</SettingItem>
<SettingItem label={t('plain_language.label')} description={t('plain_language.description')}>
<ToggleSwitch checked={settings.plainLanguage} onChange={(checked) => set('plainLanguage', checked)} />
</SettingItem>
{!isSettingHidden('density') && (
<SettingItem label={tAppearance('list_density.label')} description={tAppearance('list_density.description')} locked={isSettingLocked('density')}>
<Select
value={settings.density}
onChange={(value) => set('density', value as Density)}
options={[
{ value: 'extra-compact', label: tAppearance('list_density.extra_compact') },
{ value: 'compact', label: tAppearance('list_density.compact') },
{ value: 'regular', label: tAppearance('list_density.regular') },
{ value: 'comfortable', label: tAppearance('list_density.comfortable') },
]}
/>
</SettingItem>
)}
<SettingItem label={tAppearance('toolbar_labels.label')} description={tAppearance('toolbar_labels.description')}>
<ToggleSwitch checked={settings.showToolbarLabels} onChange={(checked) => set('showToolbarLabels', checked)} />
</SettingItem>
</SettingsSection>
<SettingsSection id="contrast" title={t('sections.contrast')}>
<SettingItem label={t('contrast.label')} description={t('contrast.description')}>
<RadioGroup
value={settings.contrast}
onChange={(value) => set('contrast', value)}
options={[
{ value: 'system', label: t('system') },
{ value: 'more', label: t('contrast.more') },
{ value: 'normal', label: t('contrast.normal') },
]}
/>
</SettingItem>
<SettingItem label={t('transparency.label')} description={t('transparency.description')}>
{systemToggle('reduceTransparency')}
</SettingItem>
<SettingItem label={t('underline_links.label')} description={t('underline_links.description')}>
<ToggleSwitch checked={settings.underlineLinks} onChange={(checked) => set('underlineLinks', checked)} />
</SettingItem>
<SettingItem label={t('color_names.label')} description={t('color_names.description')}>
<ToggleSwitch checked={settings.showColorNames} onChange={(checked) => set('showColorNames', checked)} />
</SettingItem>
<SettingItem label={t('cvd_palette.label')} description={t('cvd_palette.description')}>
<ToggleSwitch checked={settings.colorBlindPalette} onChange={(checked) => set('colorBlindPalette', checked)} />
</SettingItem>
<SettingItem label={tEmail('always_light_mode.label')} description={tEmail('always_light_mode.description')}>
<ToggleSwitch checked={settings.emailAlwaysLightMode} onChange={(checked) => set('emailAlwaysLightMode', checked)} />
</SettingItem>
<SettingItem label={t('theme_link.label')} description={t('theme_link.description')}>
<LinkButton label={t('theme_link.button')} onClick={() => goToSettings('appearance', 'theme')} />
</SettingItem>
</SettingsSection>
<SettingsSection id="motion" title={t('sections.motion')}>
<SettingItem label={t('reduce_motion.label')} description={t('reduce_motion.description')}>
{systemToggle('reduceMotion')}
</SettingItem>
<SettingItem label={t('pause_images.label')} description={t('pause_images.description')}>
{systemToggle('pauseAnimatedImages')}
</SettingItem>
</SettingsSection>
<SettingsSection id="keyboard" title={t('sections.keyboard')}>
{!isSettingHidden('singleKeyShortcuts') && (
<SettingItem label={t('single_key_shortcuts.label')} description={t('single_key_shortcuts.description')} locked={isSettingLocked('singleKeyShortcuts')}>
<ToggleSwitch checked={settings.singleKeyShortcuts} onChange={(checked) => set('singleKeyShortcuts', checked)} />
</SettingItem>
)}
<SettingItem label={t('shortcut_list.label')} description={t('shortcut_list.description')}>
<Button variant="outline" size="sm" onClick={openKeyboardShortcuts}>
<Keyboard className="w-4 h-4 me-1" />
{t('shortcut_list.button')}
</Button>
</SettingItem>
<SettingItem label={t('focus_ring.label')} description={t('focus_ring.description')}>
<RadioGroup
value={settings.focusRing}
onChange={(value) => set('focusRing', value)}
options={[
{ value: 'normal', label: t('focus_ring.normal') },
{ value: 'thick', label: t('focus_ring.thick') },
{ value: 'extra', label: t('focus_ring.extra') },
]}
/>
</SettingItem>
<SettingItem label={t('focus_always.label')} description={t('focus_always.description')}>
<ToggleSwitch checked={settings.focusAlwaysVisible} onChange={(checked) => set('focusAlwaysVisible', checked)} />
</SettingItem>
</SettingsSection>
<SettingsSection id="pointer" title={t('sections.pointer')}>
<SettingItem label={t('large_targets.label')} description={t('large_targets.description')}>
{systemToggle('largeTargets')}
</SettingItem>
<SettingItem label={t('large_cursor.label')} description={t('large_cursor.description')}>
<ToggleSwitch checked={settings.largeCursor} onChange={(checked) => set('largeCursor', checked)} />
</SettingItem>
<SettingItem label={t('open_on_hover.label')} description={t('open_on_hover.description')}>
<ToggleSwitch checked={settings.openOnHover} onChange={(checked) => set('openOnHover', checked)} />
</SettingItem>
<SettingItem label={tCalendar('hover_preview')} description={tCalendar('hover_preview_desc')}>
<Select
value={settings.calendarHoverPreview}
onChange={(value) => set('calendarHoverPreview', value)}
options={[
{ value: 'instant', label: tCalendar('hover_preview_instant') },
{ value: 'delay-500ms', label: tCalendar('hover_preview_delay_500ms') },
{ value: 'delay-1s', label: tCalendar('hover_preview_delay_1s') },
{ value: 'delay-2s', label: tCalendar('hover_preview_delay_2s') },
{ value: 'off', label: tCalendar('hover_preview_off') },
]}
/>
</SettingItem>
<SettingItem label={t('row_action_button.label')} description={t('row_action_button.description')}>
<ToggleSwitch checked={settings.showRowActionButton} onChange={(checked) => set('showRowActionButton', checked)} />
</SettingItem>
<SettingItem label={t('native_scrollbars.label')} description={t('native_scrollbars.description')}>
<ToggleSwitch checked={settings.nativeScrollbars} onChange={(checked) => set('nativeScrollbars', checked)} />
</SettingItem>
<SettingItem label={t('row_actions_link.label')} description={t('row_actions_link.description')}>
<LinkButton label={t('row_actions_link.button')} onClick={() => goToSettings('message_list', 'actions')} />
</SettingItem>
</SettingsSection>
<SettingsSection id="mail" title={t('sections.mail')}>
<SettingItem label={t('mail_view.label')} description={t('mail_view.description')}>
<RadioGroup
value={settings.mailViewMode}
onChange={(value) => set('mailViewMode', value)}
options={[
{ value: 'html', label: t('mail_view.html') },
{ value: 'plain', label: t('mail_view.plain') },
]}
/>
</SettingItem>
<SettingItem label={t('mail_text_size.label')} description={t('mail_text_size.description')}>
<Select
value={String(settings.mailTextScale)}
onChange={(value) => set('mailTextScale', Number(value))}
options={MAIL_TEXT_SCALES.map((scale) => ({
value: String(scale),
label: scale === 0 ? t('mail_text_size.follow') : `${Math.round(scale * 100)} %`,
}))}
/>
</SettingItem>
<SettingItem label={t('mail_min_font_size.label')} description={t('mail_min_font_size.description')}>
<Select
value={String(settings.mailMinFontSize)}
onChange={(value) => set('mailMinFontSize', Number(value))}
options={MAIL_MIN_FONT_SIZES.map((size) => ({
value: String(size),
label: size === 0 ? t('mail_min_font_size.none') : t('mail_min_font_size.px', { size }),
}))}
/>
</SettingItem>
<SettingItem label={t('mail_override.label')} description={t('mail_override.description')}>
<Select
value={settings.mailOverrideStyles}
onChange={(value) => set('mailOverrideStyles', value)}
options={[
{ value: 'never', label: t('mail_override.never') },
{ value: 'high-contrast', label: t('mail_override.high_contrast') },
{ value: 'always', label: t('mail_override.always') },
]}
/>
</SettingItem>
<SettingItem label={t('mail_fit_width.label')} description={t('mail_fit_width.description')}>
<ToggleSwitch checked={settings.mailFitWidth} onChange={(checked) => set('mailFitWidth', checked)} />
</SettingItem>
<SettingItem label={t('read_aloud_rate.label')} description={t('read_aloud_rate.description')}>
<RadioGroup
value={String(settings.readAloudRate)}
onChange={(value) => set('readAloudRate', Number(value))}
options={READ_ALOUD_RATES.map((rate) => ({ value: String(rate), label: `${rate}×` }))}
/>
</SettingItem>
<SettingItem label={t('read_aloud_voice.label')} description={t('read_aloud_voice.description')}>
<Select
value={settings.readAloudVoice}
onChange={(value) => set('readAloudVoice', value)}
options={readAloudVoiceOptions}
/>
</SettingItem>
<SettingItem label={t('compose_font.label')} description={t('compose_font.description')}>
<Select
value={settings.composeFontFamily}
onChange={(value) => set('composeFontFamily', value)}
options={[
{ value: '', label: t('compose_font.default') },
{ value: 'sans-serif', label: t('compose_font.sans') },
{ value: 'serif', label: t('compose_font.serif') },
{ value: 'monospace', label: t('compose_font.mono') },
]}
/>
</SettingItem>
<SettingItem label={t('compose_size.label')} description={t('compose_size.description')}>
<Select
value={settings.composeFontSize}
onChange={(value) => set('composeFontSize', value)}
options={[
{ value: '', label: t('compose_size.default') },
...COMPOSE_FONT_SIZES.map((size) => ({ value: size, label: size })),
]}
/>
</SettingItem>
<SettingItem label={tEmail('group_conversations.label')} description={tEmail('group_conversations.description')}>
<ToggleSwitch checked={!settings.disableThreading} onChange={(checked) => set('disableThreading', !checked)} />
</SettingItem>
{!isSettingHidden('showPreview') && (
<SettingItem label={tEmail('show_preview.label')} description={tEmail('show_preview.description')} locked={isSettingLocked('showPreview')}>
<ToggleSwitch checked={settings.showPreview} onChange={(checked) => set('showPreview', checked)} />
</SettingItem>
)}
{!IS_LITE && faviconsAllowed && (
<SettingItem label={tRoot('settings.advanced.sender_favicons.label')} description={tRoot('settings.advanced.sender_favicons.description')}>
<ToggleSwitch checked={settings.senderFavicons} onChange={(checked) => set('senderFavicons', checked)} />
</SettingItem>
)}
{!isSettingHidden('markAsReadDelay') && (
<SettingItem label={tEmail('mark_read.label')} description={tEmail('mark_read.description')} locked={isSettingLocked('markAsReadDelay')}>
<Select
value={settings.markAsReadDelay.toString()}
onChange={(value) => set('markAsReadDelay', parseInt(value))}
options={[
{ value: '0', label: tEmail('mark_read.instant') },
{ value: '3000', label: tEmail('mark_read.delay_3s') },
{ value: '5000', label: tEmail('mark_read.delay_5s') },
{ value: '-1', label: tEmail('mark_read.never') },
]}
/>
</SettingItem>
)}
<SettingItem label={tEmail('return_to_list_after_action.label')} description={tEmail('return_to_list_after_action.description')}>
<ToggleSwitch checked={settings.returnToListAfterAction} onChange={(checked) => set('returnToListAfterAction', checked)} />
</SettingItem>
</SettingsSection>
<SettingsSection id="timing" title={t('sections.timing')}>
<SettingItem label={t('toast_duration.label')} description={t('toast_duration.description')} locked={isSettingLocked('toastDuration')}>
<Select
value={String(settings.toastDuration)}
onChange={(value) => set('toastDuration', Number(value))}
options={toastOptions}
/>
</SettingItem>
<SettingItem label={tEmail('send_delay.label')} description={tEmail('send_delay.description')}>
<Select
value={String(settings.sendDelaySeconds)}
onChange={(value) => set('sendDelaySeconds', Number(value) as SendDelaySeconds)}
options={[
{ value: '0', label: tEmail('send_delay.off') },
{ value: '10', label: tEmail('send_delay.seconds', { seconds: 10 }) },
{ value: '30', label: tEmail('send_delay.seconds', { seconds: 30 }) },
{ value: '60', label: tEmail('send_delay.seconds', { seconds: 60 }) },
]}
/>
</SettingItem>
{!isSettingHidden('sendConfirmation') && (
<SettingItem label={t('confirm_send.label')} description={t('confirm_send.description')} locked={isSettingLocked('sendConfirmation')}>
<ToggleSwitch checked={settings.sendConfirmation} onChange={(checked) => set('sendConfirmation', checked)} />
</SettingItem>
)}
<SettingItem label={t('confirm_delete.label')} description={t('confirm_delete.description')}>
<ToggleSwitch checked={settings.confirmDelete} onChange={(checked) => set('confirmDelete', checked)} />
</SettingItem>
<SettingItem label={t('confirm_archive.label')} description={t('confirm_archive.description')}>
<ToggleSwitch checked={settings.confirmArchive} onChange={(checked) => set('confirmArchive', checked)} />
</SettingItem>
<SettingItem label={tEmail('confirm_bulk_delete.label')} description={tEmail('confirm_bulk_delete.description')}>
<ToggleSwitch checked={settings.confirmBulkDelete} onChange={(checked) => set('confirmBulkDelete', checked)} />
</SettingItem>
<SettingItem label={t('send_warnings_link.label')} description={t('send_warnings_link.description')}>
<LinkButton label={t('send_warnings_link.button')} onClick={() => goToSettings('composing', 'sending')} />
</SettingItem>
</SettingsSection>
<SettingsSection id="alerts" title={t('sections.alerts')}>
<SettingItem label={t('visual_alerts.label')} description={t('visual_alerts.description')}>
<ToggleSwitch checked={settings.visualAlerts} onChange={(checked) => set('visualAlerts', checked)} />
</SettingItem>
<SettingItem label={tNotifications('email.sound')} description={tNotifications('email.sound_desc')}>
<ToggleSwitch checked={settings.emailNotificationSound} onChange={(checked) => set('emailNotificationSound', checked)} />
</SettingItem>
<SettingItem label={tNotifications('calendar.sound')} description={tNotifications('calendar.sound_desc')}>
<ToggleSwitch checked={settings.calendarNotificationSound} onChange={(checked) => set('calendarNotificationSound', checked)} />
</SettingItem>
<SettingItem label={tNotifications('sound_selection.choose')} description={tNotifications('sound_selection.choose_desc')}>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => playNotificationSound(settings.notificationSoundChoice)}
aria-label={tNotifications('test_sound')}
title={tNotifications('test_sound')}
>
<Volume2 className="w-4 h-4" />
</Button>
<Select
value={settings.notificationSoundChoice}
onChange={(value) => {
const choice = value as NotificationSoundChoice;
set('notificationSoundChoice', choice);
playNotificationSound(choice);
}}
options={NOTIFICATION_SOUNDS.map((s) => ({ value: s.id, label: tNotifications(`sounds.${s.id}`) }))}
/>
</div>
</SettingItem>
<SettingItem label={tAppearance('favicon_unread_badge.label')} description={tAppearance('favicon_unread_badge.description')}>
<ToggleSwitch checked={settings.faviconUnreadBadge} onChange={(checked) => set('faviconUnreadBadge', checked)} />
</SettingItem>
<SettingItem label={t('unread_counts.label')} description={t('unread_counts.description')}>
<RadioGroup
value={settings.unreadCountStyle}
onChange={(value) => set('unreadCountStyle', value)}
options={[
{ value: 'count', label: t('unread_counts.count') },
{ value: 'dot', label: t('unread_counts.dot') },
{ value: 'hidden', label: t('unread_counts.hidden') },
]}
/>
</SettingItem>
<SettingItem label={t('do_not_disturb.label')} description={t('do_not_disturb.description')}>
<ToggleSwitch checked={settings.doNotDisturb} onChange={(checked) => set('doNotDisturb', checked)} />
</SettingItem>
</SettingsSection>
<SettingsSection id="screen-readers" title={t('sections.screen_readers')}>
<SettingItem label={t('announce_new_mail.label')} description={t('announce_new_mail.description')}>
<ToggleSwitch checked={settings.announceNewMail} onChange={(checked) => set('announceNewMail', checked)} />
</SettingItem>
<SettingItem label={t('announce_preview.label')} description={t('announce_preview.description')}>
<ToggleSwitch checked={settings.announceRowPreview} onChange={(checked) => set('announceRowPreview', checked)} />
</SettingItem>
<SettingItem label={t('simple_mode.label')} description={t('simple_mode.description')}>
<ToggleSwitch checked={settings.simpleMode} onChange={(checked) => set('simpleMode', checked)} />
</SettingItem>
{settings.simpleMode && (
<SettingItem nested label={t('simple_mode_paged.label')} description={t('simple_mode_paged.description')}>
<ToggleSwitch checked={settings.simpleModePagedList} onChange={(checked) => set('simpleModePagedList', checked)} />
</SettingItem>
)}
</SettingsSection>
<SettingsSection id="reset" title={t('reset.title')}>
<SettingItem label={t('reset.label')} description={t('reset.description')}>
<div className="flex flex-col items-end gap-1">
<Button variant="outline" size="sm" onClick={resetAll}>{t('reset.button')}</Button>
<p role="status" className="text-xs text-muted-foreground">{resetDone ? t('reset.done') : ''}</p>
</div>
</SettingItem>
</SettingsSection>
</div>
);
}