feat(settings): replace theme buttons with visual preview card selector

Replace simple text buttons with macOS-style window preview cards for
Light/Dark/System theme selection, using pure CSS miniature window mockups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-28 15:03:47 +08:00
parent fd9d2e2290
commit 1d08057dd8

View file

@ -1,13 +1,88 @@
"use client";
import { useTheme } from "next-themes";
import { Sun, Moon, Monitor } from "lucide-react";
import { cn } from "@/lib/utils";
const LIGHT_COLORS = {
titleBar: "#e8e8e8",
content: "#ffffff",
sidebar: "#f4f4f5",
bar: "#e4e4e7",
barMuted: "#d4d4d8",
};
const DARK_COLORS = {
titleBar: "#333338",
content: "#27272a",
sidebar: "#1e1e21",
bar: "#3f3f46",
barMuted: "#52525b",
};
function WindowMockup({
variant,
className,
}: {
variant: "light" | "dark";
className?: string;
}) {
const colors = variant === "light" ? LIGHT_COLORS : DARK_COLORS;
return (
<div className={cn("flex h-full w-full flex-col", className)}>
{/* Title bar */}
<div
className="flex items-center gap-[3px] px-2 py-1.5"
style={{ backgroundColor: colors.titleBar }}
>
<span className="size-[6px] rounded-full bg-[#ff5f57]" />
<span className="size-[6px] rounded-full bg-[#febc2e]" />
<span className="size-[6px] rounded-full bg-[#28c840]" />
</div>
{/* Content area */}
<div
className="flex flex-1"
style={{ backgroundColor: colors.content }}
>
{/* Sidebar */}
<div
className="w-[30%] space-y-1 p-2"
style={{ backgroundColor: colors.sidebar }}
>
<div
className="h-1 w-3/4 rounded-full"
style={{ backgroundColor: colors.bar }}
/>
<div
className="h-1 w-1/2 rounded-full"
style={{ backgroundColor: colors.bar }}
/>
</div>
{/* Main */}
<div className="flex-1 space-y-1.5 p-2">
<div
className="h-1.5 w-4/5 rounded-full"
style={{ backgroundColor: colors.bar }}
/>
<div
className="h-1 w-full rounded-full"
style={{ backgroundColor: colors.barMuted }}
/>
<div
className="h-1 w-3/5 rounded-full"
style={{ backgroundColor: colors.barMuted }}
/>
</div>
</div>
</div>
);
}
const themeOptions = [
{ value: "light", label: "Light", icon: Sun },
{ value: "dark", label: "Dark", icon: Moon },
{ value: "system", label: "System", icon: Monitor },
] as const;
{ value: "light" as const, label: "Light" },
{ value: "dark" as const, label: "Dark" },
{ value: "system" as const, label: "System" },
];
export function AppearanceTab() {
const { theme, setTheme } = useTheme();
@ -16,21 +91,51 @@ export function AppearanceTab() {
<div className="space-y-8">
<section className="space-y-4">
<h2 className="text-sm font-semibold">Theme</h2>
<div className="flex gap-2">
<div className="flex gap-6" role="radiogroup" aria-label="Theme">
{themeOptions.map((opt) => {
const active = theme === opt.value;
return (
<button
key={opt.value}
role="radio"
aria-checked={active}
aria-label={`Select ${opt.label} theme`}
onClick={() => setTheme(opt.value)}
className={`inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
active
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
className="group flex flex-col items-center gap-2"
>
<opt.icon className="h-3.5 w-3.5" />
{opt.label}
<div
className={cn(
"aspect-[4/3] w-36 overflow-hidden rounded-lg ring-1 transition-all",
active
? "ring-2 ring-brand"
: "ring-border hover:ring-2 hover:ring-border"
)}
>
{opt.value === "system" ? (
<div className="relative h-full w-full">
<WindowMockup
variant="light"
className="absolute inset-0"
/>
<WindowMockup
variant="dark"
className="absolute inset-0 [clip-path:inset(0_0_0_50%)]"
/>
</div>
) : (
<WindowMockup variant={opt.value} />
)}
</div>
<span
className={cn(
"text-sm transition-colors",
active
? "font-medium text-foreground"
: "text-muted-foreground"
)}
>
{opt.label}
</span>
</button>
);
})}