Redesign Settings with Apple-inspired grouped UI

Rework the Settings hub and sub-pages into iOS-style grouped inset cards:
- Hub groups rows into Account / App / System sections, each row gets a
  colored gradient icon tile, with a rounded bg-card container and hairline
  dividers instead of the flat full-width list.
- Appearance and Language pickers use the same inset-card list rather than
  bordered selectable buttons.
- Network and Advanced replace the heavy accent-bar section headers with
  uppercase group labels above clean bg-card panels.
- Add settings.groups.{account,app,system} strings across all locales.
This commit is contained in:
Alex Gleason
2026-06-12 13:00:13 -05:00
parent 59c0d25fa6
commit e8a9f679f9
21 changed files with 244 additions and 118 deletions
+5
View File
@@ -913,6 +913,11 @@
"title": "الإعدادات",
"description": "إدارة إعدادات {{appName}} الخاصة بك",
"deleteAccount": "حذف الحساب",
"groups": {
"account": "الحساب",
"app": "التطبيق",
"system": "النظام"
},
"sections": {
"profile": "الملف الشخصي",
"profileDesc": "اسم العرض والسيرة الذاتية والصورة الرمزية والتحقق.",
+5
View File
@@ -1376,6 +1376,11 @@
"title": "Settings",
"description": "Manage your {{appName}} settings",
"deleteAccount": "Delete account",
"groups": {
"account": "Account",
"app": "App",
"system": "System"
},
"sections": {
"profile": "Profile",
"profileDesc": "Display name, bio, avatar, and verification.",
+5
View File
@@ -923,6 +923,11 @@
"title": "Ajustes",
"description": "Administra tus ajustes de {{appName}}",
"deleteAccount": "Eliminar cuenta",
"groups": {
"account": "Cuenta",
"app": "Aplicación",
"system": "Sistema"
},
"sections": {
"profile": "Perfil",
"profileDesc": "Nombre, biografía, avatar y verificación.",
+5
View File
@@ -923,6 +923,11 @@
"title": "تنظیمات",
"description": "مدیریت تنظیمات {{appName}} شما",
"deleteAccount": "حذف حساب",
"groups": {
"account": "حساب",
"app": "برنامه",
"system": "سیستم"
},
"sections": {
"profile": "نمایه",
"profileDesc": "نام نمایشی، بیوگرافی، آواتار و تأیید هویت.",
+5
View File
@@ -1362,6 +1362,11 @@
"title": "Paramètres",
"description": "Gérez vos paramètres {{appName}}",
"deleteAccount": "Supprimer le compte",
"groups": {
"account": "Compte",
"app": "Application",
"system": "Système"
},
"sections": {
"profile": "Profil",
"profileDesc": "Nom d'affichage, biographie, avatar et vérification.",
+5
View File
@@ -1365,6 +1365,11 @@
"title": "सेटिंग्स",
"description": "अपनी {{appName}} सेटिंग्स प्रबंधित करें",
"deleteAccount": "अकाउंट डिलीट करें",
"groups": {
"account": "खाता",
"app": "ऐप",
"system": "सिस्टम"
},
"sections": {
"profile": "प्रोफ़ाइल",
"profileDesc": "डिस्प्ले नाम, बायो, अवतार, और सत्यापन।",
+5
View File
@@ -1365,6 +1365,11 @@
"title": "Pengaturan",
"description": "Kelola pengaturan {{appName}} Anda",
"deleteAccount": "Hapus akun",
"groups": {
"account": "Akun",
"app": "Aplikasi",
"system": "Sistem"
},
"sections": {
"profile": "Profil",
"profileDesc": "Nama tampilan, bio, avatar, dan verifikasi.",
+5
View File
@@ -923,6 +923,11 @@
"title": "ការកំណត់",
"description": "គ្រប់គ្រងការកំណត់ {{appName}} របស់អ្នក",
"deleteAccount": "លុបគណនី",
"groups": {
"account": "គណនី",
"app": "កម្មវិធី",
"system": "ប្រព័ន្ធ"
},
"sections": {
"profile": "ប្រវត្តិរូប",
"profileDesc": "ឈ្មោះបង្ហាញ ប្រវត្តិរូបសង្ខេប រូបតំណាង និងការផ្ទៀងផ្ទាត់។",
+5
View File
@@ -925,6 +925,11 @@
"title": "تنظیمات",
"description": "د {{appName}} تنظیماتو اداره کول",
"deleteAccount": "حساب ړنګول",
"groups": {
"account": "حساب",
"app": "اپلیکیشن",
"system": "سیسټم"
},
"sections": {
"profile": "پروفایل",
"profileDesc": "د ښودنې نوم، بیوګرافي، اواتار، او تصدیق.",
+5
View File
@@ -1367,6 +1367,11 @@
"title": "Configurações",
"description": "Gerencie suas configurações do {{appName}}",
"deleteAccount": "Excluir conta",
"groups": {
"account": "Conta",
"app": "Aplicativo",
"system": "Sistema"
},
"sections": {
"profile": "Perfil",
"profileDesc": "Nome de exibição, biografia, avatar e verificação.",
+5
View File
@@ -1367,6 +1367,11 @@
"title": "Настройки",
"description": "Управляйте настройками {{appName}}",
"deleteAccount": "Удалить аккаунт",
"groups": {
"account": "Аккаунт",
"app": "Приложение",
"system": "Система"
},
"sections": {
"profile": "Профиль",
"profileDesc": "Имя для отображения, биография, аватар и верификация.",
+5
View File
@@ -925,6 +925,11 @@
"title": "Marongero",
"description": "Tarisira marongero ako e{{appName}}",
"deleteAccount": "Bvisa akaunti",
"groups": {
"account": "Akaundi",
"app": "Puroguramu",
"system": "Sisitimu"
},
"sections": {
"profile": "Profile",
"profileDesc": "Zita rinoratidzwa, nhoroondo, mufananidzo, nokutsigirwa.",
+5
View File
@@ -1364,6 +1364,11 @@
"title": "Mipangilio",
"description": "Simamia mipangilio yako ya {{appName}}",
"deleteAccount": "Futa akaunti",
"groups": {
"account": "Akaunti",
"app": "Programu",
"system": "Mfumo"
},
"sections": {
"profile": "Wasifu",
"profileDesc": "Jina la kuonyesha, wasifu, picha, na uthibitisho.",
+5
View File
@@ -1366,6 +1366,11 @@
"title": "Ayarlar",
"description": "{{appName}} ayarlarınızı yönetin",
"deleteAccount": "Hesabı sil",
"groups": {
"account": "Hesap",
"app": "Uygulama",
"system": "Sistem"
},
"sections": {
"profile": "Profil",
"profileDesc": "Görünen ad, biyografi, avatar ve doğrulama.",
+5
View File
@@ -925,6 +925,11 @@
"title": "設定",
"description": "管理你的 {{appName}} 設定",
"deleteAccount": "刪除賬戶",
"groups": {
"account": "帳戶",
"app": "應用程式",
"system": "系統"
},
"sections": {
"profile": "個人資料",
"profileDesc": "顯示名稱、簡介、頭像和驗證。",
+5
View File
@@ -925,6 +925,11 @@
"title": "设置",
"description": "管理你的 {{appName}} 设置",
"deleteAccount": "删除账户",
"groups": {
"account": "账户",
"app": "应用",
"system": "系统"
},
"sections": {
"profile": "个人资料",
"profileDesc": "显示名称、简介、头像和验证。",
+4 -4
View File
@@ -37,11 +37,11 @@ export function AdvancedSettingsPage() {
}
/>
<div className="p-4">
<div className="p-4 max-w-2xl mx-auto w-full">
{/* Intro */}
<div className="px-3 pt-2 pb-4">
<h2 className="text-sm font-semibold">{t('settings.advanced.powerUserHeading')}</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
<div className="px-1 pt-1 pb-4">
<h2 className="text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80">{t('settings.advanced.powerUserHeading')}</h2>
<p className="text-[13px] text-muted-foreground mt-1.5 leading-relaxed">
{t('settings.advanced.intro')}
</p>
</div>
+14 -19
View File
@@ -1,5 +1,5 @@
import { useSeoMeta } from '@unhead/react';
import { Monitor, Moon, Sun } from 'lucide-react';
import { Check, Monitor, Moon, Sun } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { PageHeader } from '@/components/PageHeader';
import { useAppContext } from '@/hooks/useAppContext';
@@ -60,52 +60,47 @@ export function AppearanceSettingsPage() {
}
/>
<div className="p-4">
<div className="p-4 max-w-2xl mx-auto w-full">
{/* Intro */}
<div className="px-3 pt-2 pb-6">
<h2 className="text-sm font-semibold">{t('settings.appearance.colorMode')}</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
<div className="px-1 pt-1 pb-4">
<h2 className="text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80">{t('settings.appearance.colorMode')}</h2>
<p className="text-[13px] text-muted-foreground mt-1.5 leading-relaxed">
{t('settings.appearance.intro')}
</p>
</div>
{/* Theme options */}
<div className="space-y-2">
<div className="overflow-hidden rounded-2xl bg-card border border-border/60 shadow-sm divide-y divide-border/50">
{themeOptions.map((option) => (
<button
key={option.value}
onClick={() => setTheme(option.value)}
className={cn(
'w-full flex items-center gap-4 px-4 py-3.5 rounded-xl border-2 transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
theme === option.value
? 'border-primary bg-primary/5 shadow-sm'
: 'border-border/50 hover:border-primary/40 hover:bg-muted/30',
'w-full flex items-center gap-3.5 px-3.5 py-3 transition-colors',
'hover:bg-muted/50 active:bg-muted/70',
'focus-visible:outline-none focus-visible:bg-muted/50',
)}
>
<div
className={cn(
'flex items-center justify-center size-10 rounded-lg transition-colors',
'flex items-center justify-center size-9 rounded-[10px] transition-colors',
theme === option.value
? 'bg-primary text-primary-foreground'
? 'bg-primary text-primary-foreground shadow-sm'
: 'bg-muted text-muted-foreground',
)}
>
{option.icon}
</div>
<div className="flex-1 text-left min-w-0">
<p className={cn(
'text-sm font-semibold',
theme === option.value ? 'text-foreground' : 'text-foreground',
)}>
<p className="text-[15px] font-medium leading-tight text-foreground">
{t(option.labelKey)}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
<p className="text-[13px] text-muted-foreground mt-0.5 leading-snug">
{t(option.descriptionKey)}
</p>
</div>
{theme === option.value && (
<div className="size-2.5 rounded-full bg-primary shrink-0 animate-in fade-in zoom-in duration-200" />
<Check className="size-[18px] text-primary shrink-0 animate-in fade-in zoom-in duration-200" aria-hidden="true" />
)}
</button>
))}
+16 -21
View File
@@ -63,14 +63,18 @@ export function LanguageSettingsPage() {
}
/>
<div className="p-4">
<div className="px-3 pt-2 pb-6">
<p className="text-xs text-muted-foreground leading-relaxed">
<div className="p-4 max-w-2xl mx-auto w-full">
<div className="px-1 pt-1 pb-4">
<p className="text-[13px] text-muted-foreground leading-relaxed">
{t('language.intro')}
</p>
</div>
<ul className="space-y-2" role="radiogroup" aria-label={t('language.title')}>
<ul
className="overflow-hidden rounded-2xl bg-card border border-border/60 shadow-sm divide-y divide-border/50"
role="radiogroup"
aria-label={t('language.title')}
>
{SUPPORTED_LANGUAGES.map((language) => {
const selected = language.code === currentLng;
const rtl = isRTLLanguage(language.code);
@@ -82,34 +86,25 @@ export function LanguageSettingsPage() {
aria-checked={selected}
onClick={() => handleSelect(language.code)}
className={cn(
'w-full flex items-center gap-4 px-4 py-3.5 rounded-xl border-2 transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
selected
? 'border-primary bg-primary/5 shadow-sm'
: 'border-border/50 hover:border-primary/40 hover:bg-muted/30',
'w-full flex items-center gap-4 px-3.5 py-3 transition-colors',
'hover:bg-muted/50 active:bg-muted/70',
'focus-visible:outline-none focus-visible:bg-muted/50',
)}
>
<div
className={cn(
'flex-1 text-left min-w-0',
// Force native-name rendering in the locale's own
// direction so RTL scripts (Arabic, Farsi, Pashto)
// display correctly even when the surrounding UI is LTR.
)}
>
<div className="flex-1 text-left min-w-0">
<p
className="text-base font-semibold truncate"
className="text-[15px] font-medium leading-tight truncate"
dir={rtl ? 'rtl' : 'ltr'}
lang={language.code}
>
{language.nativeName}
</p>
<p className="text-xs text-muted-foreground mt-0.5 uppercase tracking-wide">
<p className="text-[12px] text-muted-foreground mt-0.5 uppercase tracking-wide">
{language.code}
</p>
</div>
{selected && (
<Check className="size-5 text-primary shrink-0" aria-hidden="true" />
<Check className="size-[18px] text-primary shrink-0" aria-hidden="true" />
)}
</button>
</li>
@@ -117,7 +112,7 @@ export function LanguageSettingsPage() {
})}
</ul>
<p className="text-xs text-muted-foreground mt-6 px-3 leading-relaxed">
<p className="text-[13px] text-muted-foreground mt-5 px-1 leading-relaxed">
{t('language.translationNote')}
</p>
</div>
+27 -43
View File
@@ -50,28 +50,25 @@ export function NetworkSettingsPage() {
}
/>
<div className="p-4">
<div className="p-4 max-w-2xl mx-auto w-full space-y-7">
{/* Intro */}
<div className="px-3 pt-2 pb-4">
<h2 className="text-sm font-semibold">{t('settings.network.connectionsHeading')}</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
<div className="px-1 pt-1">
<h2 className="text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80">{t('settings.network.connectionsHeading')}</h2>
<p className="text-[13px] text-muted-foreground mt-1.5 leading-relaxed">
{t('settings.network.connectionsIntro')}
</p>
</div>
{/* Low-Bandwidth Mode */}
<div>
<div className="relative px-3 py-3.5">
<h2 className="text-base font-semibold">{t('settings.network.lowBandwidthHeading')}</h2>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-full" />
</div>
<div className="pt-4 pb-4 px-3 space-y-4">
<section>
<h2 className="px-1 pb-1.5 text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80">{t('settings.network.lowBandwidthHeading')}</h2>
<div className="rounded-2xl bg-card border border-border/60 shadow-sm p-4 space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<Label htmlFor="low-bandwidth" className="text-sm font-medium">
<Label htmlFor="low-bandwidth" className="text-[15px] font-medium">
{t('settings.network.reduceDataUsage')}
</Label>
<p className="text-xs text-muted-foreground leading-relaxed">
<p className="text-[13px] text-muted-foreground leading-relaxed">
{t('settings.network.reduceDataUsageDesc')}
</p>
</div>
@@ -86,13 +83,13 @@ export function NetworkSettingsPage() {
{/* Image Proxy — independent of Low-Bandwidth. Controls whether
images are fetched from a downsizing proxy. */}
<div className="space-y-3">
<div className="space-y-3 pt-4 border-t border-border/50">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<Label htmlFor="image-proxy" className="text-sm font-medium">
<Label htmlFor="image-proxy" className="text-[15px] font-medium">
{t('settings.network.useImageProxy')}
</Label>
<p className="text-xs text-muted-foreground leading-relaxed">
<p className="text-[13px] text-muted-foreground leading-relaxed">
{t('settings.network.useImageProxyDesc')}
</p>
</div>
@@ -156,40 +153,27 @@ export function NetworkSettingsPage() {
)}
</div>
</div>
</div>
</section>
{/* Relays */}
<div>
<div className="relative px-3 py-3.5">
<h2 className="text-base font-semibold flex items-center gap-1.5">{t('settings.network.relays')} <HelpTip faqId="what-are-relays" /></h2>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-full" />
</div>
<div className="pt-2 pb-4">
<RelayListManager />
</div>
</div>
<section>
<h2 className="px-1 pb-1.5 text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80 flex items-center gap-1.5">{t('settings.network.relays')} <HelpTip faqId="what-are-relays" /></h2>
<RelayListManager />
</section>
{/* Blossom Servers */}
<div>
<div className="relative px-3 py-3.5">
<h2 className="text-base font-semibold flex items-center gap-1.5">{t('settings.network.blossomServers')} <HelpTip faqId="what-are-blossom" /></h2>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-full" />
</div>
<div className="pt-2 pb-4">
<BlossomSettings />
</div>
</div>
<section>
<h2 className="px-1 pb-1.5 text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80 flex items-center gap-1.5">{t('settings.network.blossomServers')} <HelpTip faqId="what-are-blossom" /></h2>
<BlossomSettings />
</section>
{/* Image Upload Quality */}
<div>
<div className="relative px-3 py-3.5">
<h2 className="text-base font-semibold">{t('settings.network.imageUploads')}</h2>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-full" />
</div>
<div className="pt-4 pb-4 px-3 space-y-3">
<section>
<h2 className="px-1 pb-1.5 text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80">{t('settings.network.imageUploads')}</h2>
<div className="rounded-2xl bg-card border border-border/60 shadow-sm p-4 space-y-3">
<div className="space-y-1.5">
<Label className="text-sm font-medium">{t('settings.network.uploadQuality')}</Label>
<p className="text-xs text-muted-foreground leading-relaxed">
<Label className="text-[15px] font-medium">{t('settings.network.uploadQuality')}</Label>
<p className="text-[13px] text-muted-foreground leading-relaxed">
{t('settings.network.uploadQualityDesc')}
</p>
</div>
@@ -210,7 +194,7 @@ export function NetworkSettingsPage() {
))}
</div>
</div>
</div>
</section>
</div>
</main>
);
+103 -31
View File
@@ -1,12 +1,24 @@
import { useSeoMeta } from '@unhead/react';
import { lazy, Suspense, useState } from 'react';
import { ChevronRight, Settings } from 'lucide-react';
import {
ChevronRight,
Settings,
User,
BadgeCheck,
Palette,
Languages,
Wifi,
Bell,
SlidersHorizontal,
ShieldCheck,
} from 'lucide-react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { PageHeader } from '@/components/PageHeader';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { isAdmin } from '@/lib/admins';
import { cn } from '@/lib/utils';
const RequestToVanishDialog = lazy(() => import('@/components/RequestToVanishDialog').then(m => ({ default: m.RequestToVanishDialog })));
@@ -17,6 +29,12 @@ interface SettingsSection {
/** i18n key under `settings.sections.*` for the row description. */
descriptionKey: string;
path: string;
/** Icon rendered in the colored leading tile. */
icon: React.ReactNode;
/** Tailwind classes for the icon tile background gradient. */
tile: string;
/** Which visual group this row belongs to. */
group: 'account' | 'app' | 'system';
requiresAuth?: boolean;
/** When true, only shown to platform admins (see `isAdmin` in `@/lib/admins`). */
requiresAdmin?: boolean;
@@ -28,6 +46,9 @@ const settingsSections: SettingsSection[] = [
labelKey: 'settings.sections.profile',
descriptionKey: 'settings.sections.profileDesc',
path: '/settings/profile',
icon: <User className="size-[18px]" />,
tile: 'bg-gradient-to-b from-blue-400 to-blue-600',
group: 'account',
requiresAuth: true,
},
{
@@ -35,6 +56,9 @@ const settingsSections: SettingsSection[] = [
labelKey: 'settings.sections.verifier',
descriptionKey: 'settings.sections.verifierDesc',
path: '/settings/verifier',
icon: <BadgeCheck className="size-[18px]" />,
tile: 'bg-gradient-to-b from-emerald-400 to-emerald-600',
group: 'account',
requiresAuth: true,
},
{
@@ -42,25 +66,37 @@ const settingsSections: SettingsSection[] = [
labelKey: 'settings.sections.appearance',
descriptionKey: 'settings.sections.appearanceDesc',
path: '/settings/appearance',
icon: <Palette className="size-[18px]" />,
tile: 'bg-gradient-to-b from-violet-400 to-violet-600',
group: 'app',
},
{
id: 'language',
labelKey: 'settings.sections.language',
descriptionKey: 'settings.sections.languageDesc',
path: '/settings/language',
},
{
id: 'network',
labelKey: 'settings.sections.network',
descriptionKey: 'settings.sections.networkDesc',
path: '/settings/network',
requiresAuth: true,
icon: <Languages className="size-[18px]" />,
tile: 'bg-gradient-to-b from-sky-400 to-sky-600',
group: 'app',
},
{
id: 'notifications',
labelKey: 'settings.sections.notifications',
descriptionKey: 'settings.sections.notificationsDesc',
path: '/settings/notifications',
icon: <Bell className="size-[18px]" />,
tile: 'bg-gradient-to-b from-rose-400 to-rose-600',
group: 'app',
requiresAuth: true,
},
{
id: 'network',
labelKey: 'settings.sections.network',
descriptionKey: 'settings.sections.networkDesc',
path: '/settings/network',
icon: <Wifi className="size-[18px]" />,
tile: 'bg-gradient-to-b from-amber-400 to-amber-600',
group: 'system',
requiresAuth: true,
},
{
@@ -68,17 +104,25 @@ const settingsSections: SettingsSection[] = [
labelKey: 'settings.sections.advanced',
descriptionKey: 'settings.sections.advancedDesc',
path: '/settings/advanced',
icon: <SlidersHorizontal className="size-[18px]" />,
tile: 'bg-gradient-to-b from-slate-400 to-slate-600',
group: 'system',
},
{
id: 'organizers',
labelKey: 'settings.sections.organizers',
descriptionKey: 'settings.sections.organizersDesc',
path: '/organizers',
icon: <ShieldCheck className="size-[18px]" />,
tile: 'bg-gradient-to-b from-teal-400 to-teal-600',
group: 'system',
requiresAuth: true,
requiresAdmin: true,
},
];
const GROUP_ORDER: Array<SettingsSection['group']> = ['account', 'app', 'system'];
export function SettingsPage() {
const { t } = useTranslation();
const { user } = useCurrentUser();
@@ -86,7 +130,6 @@ export function SettingsPage() {
const navigate = useNavigate();
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
useSeoMeta({
title: `${t('settings.title')} | ${config.appName}`,
description: t('settings.description', { appName: config.appName }),
@@ -98,31 +141,60 @@ export function SettingsPage() {
return true;
});
const groups = GROUP_ORDER
.map((group) => ({ group, items: visibleSections.filter((s) => s.group === group) }))
.filter((g) => g.items.length > 0);
return (
<main className="min-h-screen pb-16 sidebar:pb-0">
<PageHeader title={t('settings.title')} icon={<Settings className="size-5" />} backTo="/" />
{/* Settings list */}
<nav aria-label={t('settings.title')} className="px-4 sm:px-6 pt-2">
<ul className="divide-y divide-border">
{visibleSections.map((section) => (
<li key={section.id}>
<button
type="button"
onClick={() => navigate(section.path)}
className="flex w-full items-center gap-4 px-2 py-4 text-left transition-colors hover:bg-muted/40 focus-visible:bg-muted/40 focus-visible:outline-none"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold">{t(section.labelKey)}</p>
<p className="text-xs text-muted-foreground mt-0.5">
{t(section.descriptionKey)}
</p>
</div>
<ChevronRight className="size-4 text-muted-foreground shrink-0 rtl:rotate-180" aria-hidden="true" />
</button>
</li>
))}
</ul>
{/* Grouped settings list */}
<nav aria-label={t('settings.title')} className="px-4 sm:px-6 pt-2 space-y-7 max-w-2xl mx-auto w-full">
{groups.map(({ group, items }) => (
<section key={group}>
<h2 className="px-3.5 pb-1.5 text-[12px] font-semibold uppercase tracking-wider text-muted-foreground/80">
{t(`settings.groups.${group}`)}
</h2>
<ul className="overflow-hidden rounded-2xl bg-card border border-border/60 shadow-sm divide-y divide-border/50">
{items.map((section) => (
<li key={section.id}>
<button
type="button"
onClick={() => navigate(section.path)}
className={cn(
'group flex w-full items-center gap-3.5 px-3.5 py-3 text-left',
'transition-colors hover:bg-muted/50 active:bg-muted/70',
'focus-visible:bg-muted/50 focus-visible:outline-none',
)}
>
<span
className={cn(
'flex size-8 shrink-0 items-center justify-center rounded-[9px] text-white shadow-sm',
section.tile,
)}
aria-hidden="true"
>
{section.icon}
</span>
<span className="flex-1 min-w-0">
<span className="block text-[15px] font-medium leading-tight text-foreground">
{t(section.labelKey)}
</span>
<span className="block text-[13px] text-muted-foreground mt-0.5 leading-snug">
{t(section.descriptionKey)}
</span>
</span>
<ChevronRight
className="size-4 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5 rtl:rotate-180"
aria-hidden="true"
/>
</button>
</li>
))}
</ul>
</section>
))}
</nav>
{/* Delete account */}
@@ -131,7 +203,7 @@ export function SettingsPage() {
<button
type="button"
onClick={() => setDeleteAccountOpen(true)}
className="text-xs font-medium text-destructive hover:underline focus-visible:underline focus-visible:outline-none"
className="text-[13px] font-medium text-destructive hover:underline focus-visible:underline focus-visible:outline-none"
>
{t('settings.deleteAccount')}
</button>