feat: update appearance settings page

This commit is contained in:
zhenyi 2026-05-30 15:07:56 +08:00
parent b489296b08
commit 04798b5adb

View File

@ -1,14 +1,11 @@
import { useState, useEffect, useCallback } from "react";
import { useUserConfig, useUpdateAppearance } from "./hooks"; import { useUserConfig, useUpdateAppearance } from "./hooks";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { themePresets, applyTheme, getThemeById, getSavedThemeId, saveThemeId } from "@/lib/theme";
const themes = [ import { Check } from "lucide-react";
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "System" },
];
const codeThemes = [ const codeThemes = [
{ value: "github-light", label: "GitHub Light" }, { value: "github-light", label: "GitHub Light" },
@ -26,11 +23,62 @@ const densities = [
{ value: "comfortable", label: "Comfortable" }, { value: "comfortable", label: "Comfortable" },
]; ];
function ThemeSwatch({ preset }: { preset: (typeof themePresets)[0] }) {
const c = preset.colors;
return (
<div
className="relative h-20 w-full overflow-hidden rounded-xl border"
style={{
background: c.background,
borderColor: c.border,
}}
>
<div className="absolute inset-0 flex flex-col p-2.5 gap-1.5">
<div className="flex items-center gap-1.5">
<div
className="h-3.5 w-3.5 rounded-md"
style={{ background: c.primary }}
/>
<div
className="h-1.5 w-12 rounded-full"
style={{ background: c.mutedForeground, opacity: 0.5 }}
/>
</div>
<div className="flex gap-1">
<div
className="h-5 flex-1 rounded-md"
style={{ background: c.card, border: `1px solid ${c.border}` }}
/>
<div
className="h-5 flex-1 rounded-md"
style={{ background: c.muted }}
/>
</div>
</div>
</div>
);
}
export default function SettingsAppearancePage() { export default function SettingsAppearancePage() {
const { data: config, isLoading } = useUserConfig(); const { data: config, isLoading } = useUserConfig();
const update = useUpdateAppearance(); const update = useUpdateAppearance();
const appearance = config?.appearance; const appearance = config?.appearance;
const [activeTheme, setActiveTheme] = useState(getSavedThemeId());
useEffect(() => {
setActiveTheme(getSavedThemeId());
}, []);
const handleThemeChange = useCallback((id: string) => {
setActiveTheme(id);
const preset = getThemeById(id);
if (preset) {
applyTheme(preset);
saveThemeId(id);
}
}, []);
return ( return (
<div className="px-8 py-10"> <div className="px-8 py-10">
<h1 className="text-xl font-heading font-bold text-foreground">Appearance</h1> <h1 className="text-xl font-heading font-bold text-foreground">Appearance</h1>
@ -44,28 +92,57 @@ export default function SettingsAppearancePage() {
</div> </div>
) : ( ) : (
<div className="mt-8 space-y-6"> <div className="mt-8 space-y-6">
{/* Color Theme */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Theme</CardTitle> <CardTitle>Color Theme</CardTitle>
<CardDescription>Select your preferred color theme</CardDescription> <CardDescription>Choose a preset color palette for the entire interface</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Select <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
value={appearance?.theme ?? "system"} {themePresets.map((preset) => {
onValueChange={(v) => update.mutate({ theme: v })} const isActive = activeTheme === preset.id;
> return (
<SelectTrigger className="w-48"> <button
<SelectValue /> key={preset.id}
</SelectTrigger> onClick={() => handleThemeChange(preset.id)}
<SelectContent> className="group relative flex flex-col gap-2 rounded-2xl border p-3 text-left transition-all hover:border-ring/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
{themes.map((t) => ( style={{
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem> borderColor: isActive ? preset.colors.ring : preset.colors.border,
))} background: isActive ? preset.colors.muted : preset.colors.card,
</SelectContent> }}
</Select> >
{isActive && (
<div
className="absolute top-2.5 right-2.5 grid size-5 place-items-center rounded-full"
style={{ background: preset.colors.primary }}
>
<Check className="size-3" style={{ color: preset.colors.primaryForeground }} />
</div>
)}
<ThemeSwatch preset={preset} />
<div className="flex flex-col gap-0.5">
<span
className="text-[13px] font-semibold"
style={{ color: preset.colors.foreground }}
>
{preset.name}
</span>
<span
className="text-[11px] leading-tight"
style={{ color: preset.colors.mutedForeground }}
>
{preset.description}
</span>
</div>
</button>
);
})}
</div>
</CardContent> </CardContent>
</Card> </Card>
{/* Code Theme */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Code Theme</CardTitle> <CardTitle>Code Theme</CardTitle>
@ -88,6 +165,7 @@ export default function SettingsAppearancePage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Layout Density */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Layout Density</CardTitle> <CardTitle>Layout Density</CardTitle>
@ -110,6 +188,7 @@ export default function SettingsAppearancePage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Editor */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Editor</CardTitle> <CardTitle>Editor</CardTitle>
@ -138,4 +217,4 @@ export default function SettingsAppearancePage() {
)} )}
</div> </div>
); );
} }