flatten folder design-system
This commit is contained in:
182
src/design-system/base/switch.tsx
Normal file
182
src/design-system/base/switch.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
/**
|
||||
* Switch 开关组件
|
||||
*
|
||||
* Design System 中的开关组件,用于二进制状态切换。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 默认开关
|
||||
* <Switch checked={checked} onChange={setChecked} />
|
||||
*
|
||||
* // 带标签
|
||||
* <Switch label="启用通知" checked={checked} onChange={setChecked} />
|
||||
*
|
||||
* // 不同尺寸
|
||||
* <Switch size="sm" checked={checked} onChange={setChecked} />
|
||||
* <Switch size="lg" checked={checked} onChange={setChecked} />
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* 开关变体样式
|
||||
*/
|
||||
const switchVariants = cva(
|
||||
// 基础样式
|
||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 appearance-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-gray-300 bg-gray-100 checked:border-primary-500 checked:bg-primary-500",
|
||||
success:
|
||||
"border-gray-300 bg-gray-100 checked:border-success-500 checked:bg-success-500",
|
||||
warning:
|
||||
"border-gray-300 bg-gray-100 checked:border-warning-500 checked:bg-warning-500",
|
||||
error:
|
||||
"border-gray-300 bg-gray-100 checked:border-error-500 checked:bg-error-500",
|
||||
},
|
||||
size: {
|
||||
sm: "h-5 w-9",
|
||||
md: "h-6 w-11",
|
||||
lg: "h-7 w-13",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type SwitchVariant = VariantProps<typeof switchVariants>["variant"];
|
||||
export type SwitchSize = VariantProps<typeof switchVariants>["size"];
|
||||
|
||||
export interface SwitchProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
|
||||
VariantProps<typeof switchVariants> {
|
||||
// 标签文本
|
||||
label?: React.ReactNode;
|
||||
// 标签位置
|
||||
labelPosition?: "left" | "right";
|
||||
// 自定义开关类名
|
||||
switchClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch 开关组件
|
||||
*/
|
||||
export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
(
|
||||
{
|
||||
variant = "default",
|
||||
size = "md",
|
||||
label,
|
||||
labelPosition = "right",
|
||||
className,
|
||||
switchClassName,
|
||||
disabled,
|
||||
checked,
|
||||
defaultChecked,
|
||||
onChange,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const switchId = React.useId();
|
||||
const [internalChecked, setInternalChecked] = useState(
|
||||
checked ?? defaultChecked ?? false
|
||||
);
|
||||
|
||||
// 处理受控和非受控模式
|
||||
const isControlled = checked !== undefined;
|
||||
const isChecked = isControlled ? checked : internalChecked;
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!isControlled) {
|
||||
setInternalChecked(e.target.checked);
|
||||
}
|
||||
onChange?.(e);
|
||||
};
|
||||
|
||||
// 确保 size 有默认值
|
||||
const actualSize = size ?? "md";
|
||||
|
||||
// 滑块大小
|
||||
const thumbSize = {
|
||||
sm: "h-3.5 w-3.5",
|
||||
md: "h-4 w-4",
|
||||
lg: "h-5 w-5",
|
||||
}[actualSize];
|
||||
|
||||
// 滑块位移
|
||||
const thumbTranslate = {
|
||||
sm: isChecked ? "translate-x-4" : "translate-x-0.5",
|
||||
md: isChecked ? "translate-x-5" : "translate-x-0.5",
|
||||
lg: isChecked ? "translate-x-6" : "translate-x-0.5",
|
||||
}[actualSize];
|
||||
|
||||
const renderSwitch = () => (
|
||||
<div className="relative inline-block">
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
id={switchId}
|
||||
disabled={disabled}
|
||||
checked={isChecked}
|
||||
onChange={handleChange}
|
||||
className={cn(
|
||||
switchVariants({ variant, size }),
|
||||
"peer/switch",
|
||||
switchClassName
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{/* 滑块 */}
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute top-1/2 -translate-y-1/2 rounded-full bg-white shadow-sm transition-transform duration-250",
|
||||
thumbSize,
|
||||
thumbTranslate
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLabel = () => {
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={switchId}
|
||||
className={cn(
|
||||
"text-base font-normal leading-none",
|
||||
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
||||
labelPosition === "left" ? "mr-3" : "ml-3"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
if (!label) {
|
||||
return renderSwitch();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("inline-flex items-center", className)}>
|
||||
{labelPosition === "left" && renderLabel()}
|
||||
{renderSwitch()}
|
||||
{labelPosition === "right" && renderLabel()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Switch.displayName = "Switch";
|
||||
Reference in New Issue
Block a user