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:
@@ -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>
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -10,6 +10,7 @@ const SettingsContent = lazy(() =>
|
||||
|
||||
export const SETTINGS_TABS = [
|
||||
"profile",
|
||||
"preferences",
|
||||
"security",
|
||||
"connections",
|
||||
"referral",
|
||||
|
||||
Reference in New Issue
Block a user