refactor(appearance): organize components and hooks

This commit is contained in:
Maofeng
2026-09-29 22:12:40 +08:00
parent 352db2c4c0
commit 642cb71b93
32 changed files with 497 additions and 161 deletions
@@ -0,0 +1,66 @@
import { useMessage } from "@workspace/i18n"
import { SunIcon } from "@workspace/ui/components/icon"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { Brightness } from "../../display"
import { appearanceMessages } from "../../messages"
export interface BrightnessFieldProps {
className?: string
onValueChange: (value: Brightness) => void
value: Brightness
}
export function BrightnessField({
className,
onValueChange,
value,
}: BrightnessFieldProps) {
const title = useMessage(appearanceMessages.brightness)
const dim = useMessage(appearanceMessages.brightnessDim)
const normal = useMessage(appearanceMessages.brightnessNormal)
const bright = useMessage(appearanceMessages.brightnessBright)
const description = useMessage(appearanceMessages.brightnessDescription)
const items: ReadonlyArray<{ label: string; value: Brightness }> = [
{ label: dim, value: "dim" },
{ label: normal, value: "normal" },
{ label: bright, value: "bright" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={items}
value={value}
onValueChange={(brightness) => {
if (brightness) onValueChange(brightness)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-40">
<SunIcon />
<SelectValue>
{(selectedValue: Brightness | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,75 @@
import { useMessage } from "@workspace/i18n"
import { EyeIcon } from "@workspace/ui/components/icon"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { ColorVisionMode } from "../../color-vision"
import { appearanceMessages } from "../../messages"
export interface ColorVisionModeFieldProps {
className?: string
onValueChange: (value: ColorVisionMode) => void
value: ColorVisionMode
}
export function ColorVisionModeField({
className,
onValueChange,
value,
}: ColorVisionModeFieldProps) {
const title = useMessage(appearanceMessages.colorVisionMode)
const standard = useMessage(appearanceMessages.colorVisionStandard)
const protan = useMessage(appearanceMessages.colorVisionProtan)
const deutan = useMessage(appearanceMessages.colorVisionDeutan)
const tritan = useMessage(appearanceMessages.colorVisionTritan)
const standardDescription = useMessage(
appearanceMessages.colorVisionStandardDescription
)
const assistedDescription = useMessage(
appearanceMessages.colorVisionAssistedDescription
)
const items: ReadonlyArray<{ label: string; value: ColorVisionMode }> = [
{ label: standard, value: "standard" },
{ label: protan, value: "protan" },
{ label: deutan, value: "deutan" },
{ label: tritan, value: "tritan" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{value === "standard" ? standardDescription : assistedDescription}
</p>
<Select
items={items}
value={value}
onValueChange={(mode) => {
if (mode) onValueChange(mode)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-56">
<EyeIcon />
<SelectValue>
{(selectedValue: ColorVisionMode | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,12 @@
export { BrightnessField, type BrightnessFieldProps } from "./brightness-field"
export {
ColorVisionModeField,
type ColorVisionModeFieldProps,
} from "./color-vision-mode-field"
export { MotionModeField, type MotionModeFieldProps } from "./motion-mode-field"
export { TextScaleField, type TextScaleFieldProps } from "./text-scale-field"
export {
ThemeModeField,
ThemeModeIcon,
type ThemeModeFieldProps,
} from "./theme-mode-field"
@@ -0,0 +1,70 @@
import { useMessage } from "@workspace/i18n"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { MotionMode } from "../../display"
import { appearanceMessages } from "../../messages"
export interface MotionModeFieldProps {
className?: string
onValueChange: (value: MotionMode) => void
value: MotionMode
}
export function MotionModeField({
className,
onValueChange,
value,
}: MotionModeFieldProps) {
const title = useMessage(appearanceMessages.motionMode)
const system = useMessage(appearanceMessages.motionSystem)
const reduce = useMessage(appearanceMessages.motionReduce)
const full = useMessage(appearanceMessages.motionFull)
const descriptions = {
full: useMessage(appearanceMessages.motionFullDescription),
reduce: useMessage(appearanceMessages.motionReduceDescription),
system: useMessage(appearanceMessages.motionSystemDescription),
}
const items: ReadonlyArray<{ label: string; value: MotionMode }> = [
{ label: system, value: "system" },
{ label: reduce, value: "reduce" },
{ label: full, value: "full" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{descriptions[value]}
</p>
<Select
items={items}
value={value}
onValueChange={(mode) => {
if (mode) onValueChange(mode)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-56">
<SelectValue>
{(selectedValue: MotionMode | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,64 @@
import { useMessage } from "@workspace/i18n"
import { ALargeSmall } from "@workspace/ui/components/icon"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { TextScale } from "../../display"
import { appearanceMessages } from "../../messages"
export interface TextScaleFieldProps {
className?: string
onValueChange: (value: TextScale) => void
value: TextScale
}
export function TextScaleField({
className,
onValueChange,
value,
}: TextScaleFieldProps) {
const title = useMessage(appearanceMessages.textScale)
const description = useMessage(appearanceMessages.textScaleDescription)
const items: ReadonlyArray<{ label: string; value: TextScale }> = [
{ label: "100%", value: "100" },
{ label: "125%", value: "125" },
{ label: "150%", value: "150" },
{ label: "200%", value: "200" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={items}
value={value}
onValueChange={(scale) => {
if (scale) onValueChange(scale)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-40">
<ALargeSmall />
<SelectValue>
{(selectedValue: TextScale | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,84 @@
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { useMessage } from "@workspace/i18n"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../../messages"
import type { ThemeMode } from "../../preferences"
export interface ThemeModeFieldProps {
className?: string
onValueChange: (value: ThemeMode) => void
value: ThemeMode
}
export function ThemeModeIcon({ mode }: { mode: ThemeMode | null }) {
if (mode === "light") return <SunIcon />
if (mode === "dark") return <MoonIcon />
return <MonitorIcon />
}
export function ThemeModeField({
className,
onValueChange,
value,
}: ThemeModeFieldProps) {
const lightLabel = useMessage(appearanceMessages.lightMode)
const darkLabel = useMessage(appearanceMessages.darkMode)
const systemLabel = useMessage(appearanceMessages.followSystem)
const lightDescription = useMessage(appearanceMessages.lightModeDescription)
const darkDescription = useMessage(appearanceMessages.darkModeDescription)
const systemDescription = useMessage(
appearanceMessages.followSystemDescription
)
const title = useMessage(appearanceMessages.themeMode)
const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [
{ label: lightLabel, value: "light" },
{ label: darkLabel, value: "dark" },
{ label: systemLabel, value: "system" },
]
const description = {
light: lightDescription,
dark: darkDescription,
system: systemDescription,
}[value]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={modeItems}
value={value}
onValueChange={(mode) => {
if (mode) onValueChange(mode)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-40">
<SelectValue>
{(selectedValue: ThemeMode | null) => (
<>
<ThemeModeIcon mode={selectedValue} />
{modeItems.find((item) => item.value === selectedValue)?.label}
</>
)}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{modeItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
<ThemeModeIcon mode={item.value} />
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}