flatten folder design-system

This commit is contained in:
2026-02-24 07:56:21 +08:00
parent 690222ccb7
commit 72ced7866e
51 changed files with 94 additions and 95 deletions

View 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";