import { useState, type ButtonHTMLAttributes, type ReactNode } from "react"; import { cn } from "@/lib/utils"; /** * Switch — 开关(自研,button role=switch,无依赖)。 * * 设计约束(对齐 ui/README 与 ADR-0008): * - 承载语义与键盘(Space/Enter 原生), * :focus-visible outline 落自身;无裸色、只消费三层 token。 * - 开 = bg-foreground + 反色 thumb(bg-background 语义对齐 Checkbox 选中态), * 关 = bg-surface-tertiary + 发丝内描边 + surface-strong 抬升 thumb(同 SegmentedControl 选中面)。 * - thumb 滑动用 translate-x 位移(尺寸档固定行程),motion-reduce 关闭过渡。 * - data-state="on|off" 暴露给上下文 CSS 覆盖(admin 语义绿等保留于页面层,不侵入组件)。 * - API 对齐 AntD Switch:checked/defaultChecked/onChange/disabled/size/checkedChildren/ * unCheckedChildren/loading + aria-* 透传;受控与否由 checked 是否传入决定, * onChange(checked) 单参。顶层组件接收 checked/onChange → Form.Item valuePropName="checked" 注入兼容。 * 对标:AntD Switch;boardui base/switch。 */ export interface SwitchProps extends Omit, "type" | "size" | "onChange"> { /** 开关尺寸。默认 md;AntD small → sm 映射。 */ size?: "sm" | "md"; /** 受控:是否开启 */ checked?: boolean; /** 非受控初始值 */ defaultChecked?: boolean; /** loading:拇指位换转圈,忽略点击(视觉保留 checked 态) */ loading?: boolean; /** 开启时轨道内左侧文案(2 字宽度档) */ checkedChildren?: ReactNode; /** 关闭时轨道内右侧文案 */ unCheckedChildren?: ReactNode; /** 变化回调(AntD 同签名:仅布尔参) */ onChange?: (checked: boolean) => void; } /** 几何档:轨道尺寸与 thumb 行程(thumb 用 left-0.5 起始 + translate-x 档位) */ const SWITCH_SIZE: Record<"sm" | "md", { root: string; thumbPlain: string; thumbText: string; hasTextRoot: string }> = { sm: { root: "h-5 w-8 rounded-full", thumbPlain: "size-3.5 data-[state=on]:translate-x-3.5", thumbText: "size-3.5 data-[state=on]:translate-x-3.5", hasTextRoot: "", }, md: { root: "h-6 w-11 rounded-full", thumbPlain: "size-4 data-[state=on]:translate-x-6", thumbText: "size-4 data-[state=on]:translate-x-9", hasTextRoot: "w-14", }, }; export function Switch({ size = "md", checked: checkedProp, defaultChecked = false, loading = false, checkedChildren, unCheckedChildren, onChange, disabled, className, "aria-label": ariaLabel, ...props }: SwitchProps) { const [innerChecked, setInnerChecked] = useState(defaultChecked); const controlled = checkedProp !== undefined; const checked = controlled ? checkedProp : innerChecked; const hasText = checkedChildren !== undefined || unCheckedChildren !== undefined; const handleClick = () => { if (disabled || loading) { return; } const next = !checked; if (!controlled) { setInnerChecked(next); } onChange?.(next); }; const geometry = SWITCH_SIZE[size]; const thumbClass = hasText ? geometry.thumbText : geometry.thumbPlain; return ( {/* 关态文案(右侧) */} {unCheckedChildren !== undefined ? ( {unCheckedChildren} ) : null} {/* 开态文案(左侧,反色) */} {checkedChildren !== undefined ? ( {checkedChildren} ) : null} {/* thumb / loading 转圈 */} {loading ? ( ) : ( )} ); }