feat(appearance): add theme color picker, zoom level selector, reset button
外观设置页新增三个 UI 单元:12 个 shadcn 主题预设的色盘按钮网格、6 档窗口缩放 下拉选择器、以及只重置主题色和缩放(不动主题模式)的恢复默认按钮。 i18n 键在上一个 Task 中已预备好,本提交后整个外观设置功能即可在浏览器中端到端使用。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Monitor, Moon, Sun } from "lucide-react"
|
import { Monitor, Moon, RotateCcw, Sun, Type } from "lucide-react"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -10,12 +11,26 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
import { useThemeColor, useZoomLevel } from "@/hooks/use-appearance"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
DEFAULT_THEME_COLOR,
|
||||||
|
DEFAULT_ZOOM_LEVEL,
|
||||||
|
THEME_COLOR_PREVIEW,
|
||||||
|
THEME_COLORS,
|
||||||
|
ZOOM_LEVELS,
|
||||||
|
type ThemeColor,
|
||||||
|
type ZoomLevel,
|
||||||
|
} from "@/lib/theme-presets"
|
||||||
|
|
||||||
type ThemeMode = "system" | "light" | "dark"
|
type ThemeMode = "system" | "light" | "dark"
|
||||||
|
|
||||||
export function AppearanceSettings() {
|
export function AppearanceSettings() {
|
||||||
const t = useTranslations("AppearanceSettings")
|
const t = useTranslations("AppearanceSettings")
|
||||||
const { theme, resolvedTheme, setTheme } = useTheme()
|
const { theme, resolvedTheme, setTheme } = useTheme()
|
||||||
|
const { themeColor, setThemeColor } = useThemeColor()
|
||||||
|
const { zoomLevel, setZoomLevel } = useZoomLevel()
|
||||||
|
|
||||||
const resolvedThemeLabel =
|
const resolvedThemeLabel =
|
||||||
resolvedTheme === "dark"
|
resolvedTheme === "dark"
|
||||||
? t("resolvedTheme.dark")
|
? t("resolvedTheme.dark")
|
||||||
@@ -23,9 +38,18 @@ export function AppearanceSettings() {
|
|||||||
? t("resolvedTheme.light")
|
? t("resolvedTheme.light")
|
||||||
: t("resolvedTheme.unknown")
|
: t("resolvedTheme.unknown")
|
||||||
|
|
||||||
|
const isAtDefaults =
|
||||||
|
themeColor === DEFAULT_THEME_COLOR && zoomLevel === DEFAULT_ZOOM_LEVEL
|
||||||
|
|
||||||
|
const handleResetToDefaults = () => {
|
||||||
|
setThemeColor(DEFAULT_THEME_COLOR)
|
||||||
|
setZoomLevel(DEFAULT_ZOOM_LEVEL)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<div className="w-full space-y-4">
|
<div className="w-full space-y-4">
|
||||||
|
{/* ===== Theme Mode (existing) ===== */}
|
||||||
<section className="rounded-xl border bg-card p-4 space-y-4">
|
<section className="rounded-xl border bg-card p-4 space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Sun className="h-4 w-4 text-muted-foreground" />
|
<Sun className="h-4 w-4 text-muted-foreground" />
|
||||||
@@ -76,6 +100,112 @@ export function AppearanceSettings() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* ===== Theme Color (new) ===== */}
|
||||||
|
<section className="rounded-xl border bg-card p-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="size-4 rounded-full border"
|
||||||
|
style={{ backgroundColor: THEME_COLOR_PREVIEW[themeColor] }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<h2 className="text-sm font-semibold">
|
||||||
|
{t("themeColor.sectionTitle")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground leading-5">
|
||||||
|
{t("themeColor.sectionDescription")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-6">
|
||||||
|
{THEME_COLORS.map((color) => {
|
||||||
|
const isActive = themeColor === color
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setThemeColor(color as ThemeColor)}
|
||||||
|
aria-pressed={isActive}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md border px-3 py-2 text-xs transition-colors",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
isActive && "border-primary ring-2 ring-primary/30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="size-4 shrink-0 rounded-full border"
|
||||||
|
style={{ backgroundColor: THEME_COLOR_PREVIEW[color] }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className="truncate">
|
||||||
|
{t(`themeColor.options.${color}`)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{t("themeColor.current", {
|
||||||
|
color: t(`themeColor.options.${themeColor}`),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ===== Zoom Level (new) ===== */}
|
||||||
|
<section className="rounded-xl border bg-card p-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Type className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<h2 className="text-sm font-semibold">
|
||||||
|
{t("zoomLevel.sectionTitle")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground leading-5">
|
||||||
|
{t("zoomLevel.sectionDescription")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Select
|
||||||
|
value={String(zoomLevel)}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setZoomLevel(parseInt(value, 10) as ZoomLevel)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-56">
|
||||||
|
<SelectValue placeholder={t("zoomLevel.placeholder")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="start">
|
||||||
|
{ZOOM_LEVELS.map((z) => (
|
||||||
|
<SelectItem key={z} value={String(z)}>
|
||||||
|
{z}%
|
||||||
|
{z === DEFAULT_ZOOM_LEVEL
|
||||||
|
? ` (${t("zoomLevel.default")})`
|
||||||
|
: ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{t("zoomLevel.current", { zoom: zoomLevel })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ===== Reset to defaults (new) ===== */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={isAtDefaults}
|
||||||
|
onClick={handleResetToDefaults}
|
||||||
|
title={t("resetHint")}
|
||||||
|
>
|
||||||
|
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||||
|
{t("resetToDefaults")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user