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:
parent
fd9d2e2290
commit
1d08057dd8
1 changed files with 118 additions and 13 deletions
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue