feat(web): add Preferences tab (theme + language) to settings

New "Preferences" settings tab giving the theme a real home in settings
(light/dark/system selector wired to userSettings + applyTheme) alongside
the language switcher, instead of theme living only in the header toggle.
Theme group uses a fieldset/legend for accessible grouping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 23:32:15 +03:00
parent 513b856bea
commit 239791290e
4 changed files with 94 additions and 0 deletions

View File

@@ -1,9 +1,11 @@
import { LanguageSwitcher } from "@/components/language-switcher";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { signIn } from "@/lib/auth-client";
import { useTranslation } from "@/lib/i18n";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
@@ -30,8 +32,12 @@ import {
EyeOff,
Gift,
Link2,
Monitor,
Moon,
Share2,
Shield,
SlidersHorizontal,
Sun,
Trash2,
User,
} from "lucide-react";
@@ -40,6 +46,7 @@ import { ChangelogTab } from "./changelog-tab";
const TAB_ITEMS = [
{ value: "profile", icon: User, labelKey: "settings.tabs.profile" },
{ value: "preferences", icon: SlidersHorizontal, labelKey: "settings.tabs.preferences" },
{ value: "security", icon: Shield, labelKey: "settings.tabs.security" },
{ value: "connections", icon: Link2, labelKey: "settings.tabs.connections" },
{ value: "referral", icon: Gift, labelKey: "settings.tabs.referral" },
@@ -47,6 +54,21 @@ const TAB_ITEMS = [
{ value: "changelog", icon: CalendarDays, labelKey: "settings.tabs.changelog" },
] as const;
type ThemePref = "light" | "dark" | "system";
const THEME_OPTIONS: { value: ThemePref; icon: typeof Sun; labelKey: string }[] = [
{ value: "light", icon: Sun, labelKey: "settings.preferences.themeLight" },
{ value: "dark", icon: Moon, labelKey: "settings.preferences.themeDark" },
{ value: "system", icon: Monitor, labelKey: "settings.preferences.themeSystem" },
];
function applyTheme(theme: ThemePref) {
const isDark =
theme === "dark" ||
(theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", isDark);
}
function PasswordField({
id,
label,
@@ -148,6 +170,15 @@ export function SettingsContent({
const passwordMismatch = confirmPassword.length > 0 && newPassword !== confirmPassword;
// Appearance
const [theme, setTheme] = useState<ThemePref>(() => getUserSettings().theme ?? "dark");
function selectTheme(next: ThemePref) {
setTheme(next);
setUserSetting("theme", next);
applyTheme(next);
}
useEffect(() => {
if (user?.name) setName(user.name);
}, [user?.name]);
@@ -296,6 +327,48 @@ export function SettingsContent({
</Card>
</TabsContent>
{/* Preferences Tab */}
<TabsContent value="preferences">
<Card>
<CardHeader>
<CardTitle>{t("settings.preferences.title")}</CardTitle>
<CardDescription>{t("settings.preferences.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<fieldset className="space-y-2">
<legend className="mb-2 text-sm font-medium">
{t("settings.preferences.theme")}
</legend>
<div className="grid grid-cols-3 gap-2 sm:max-w-md">
{THEME_OPTIONS.map(({ value, icon: Icon, labelKey }) => (
<button
key={value}
type="button"
onClick={() => selectTheme(value)}
aria-pressed={theme === value}
className={`flex flex-col items-center gap-2 rounded-lg border p-3 text-sm font-medium transition-colors ${
theme === value
? "border-brand bg-brand/10 text-foreground"
: "border-border text-muted-foreground hover:bg-accent hover:text-foreground"
}`}
>
<Icon className="size-5" />
{t(labelKey)}
</button>
))}
</div>
</fieldset>
<Separator />
<div className="space-y-2">
<Label>{t("settings.preferences.language")}</Label>
<LanguageSwitcher variant="inline" />
</div>
</CardContent>
</Card>
</TabsContent>
{/* Security Tab */}
<TabsContent value="security">
<Card>

View File

@@ -388,12 +388,22 @@
"title": "Settings",
"tabs": {
"profile": "Profile",
"preferences": "Preferences",
"security": "Security",
"connections": "Connections",
"referral": "Referral",
"account": "Account",
"changelog": "Changelog"
},
"preferences": {
"title": "Preferences",
"description": "Manage your appearance and language preferences.",
"theme": "Theme",
"themeLight": "Light",
"themeDark": "Dark",
"themeSystem": "System",
"language": "Language"
},
"profile": {
"title": "Profile Information",
"description": "Update your personal information.",

View File

@@ -388,12 +388,22 @@
"title": "Ayarlar",
"tabs": {
"profile": "Profil",
"preferences": "Tercihler",
"security": "Güvenlik",
"connections": "Bağlantılar",
"referral": "Referans",
"account": "Hesap",
"changelog": "Değişiklik Günlüğü"
},
"preferences": {
"title": "Tercihler",
"description": "Görünüm ve dil tercihlerinizi yönetin.",
"theme": "Tema",
"themeLight": "Açık",
"themeDark": "Koyu",
"themeSystem": "Sistem",
"language": "Dil"
},
"profile": {
"title": "Profil Bilgileri",
"description": "Kişisel bilgilerinizi güncelleyin.",

View File

@@ -10,6 +10,7 @@ const SettingsContent = lazy(() =>
export const SETTINGS_TABS = [
"profile",
"preferences",
"security",
"connections",
"referral",