Design System 重构继续完成
This commit is contained in:
@@ -41,7 +41,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
const buttonVariants = cva(
|
||||
// 基础样式
|
||||
"inline-flex items-center justify-center gap-2 rounded-xl font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center gap-2 rounded-md font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -51,6 +51,7 @@ const buttonVariants = cva(
|
||||
warning: "bg-warning-500 text-white hover:bg-warning-600 shadow-md",
|
||||
error: "bg-error-500 text-white hover:bg-error-600 shadow-md",
|
||||
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 shadow-none",
|
||||
"ghost-light": "bg-transparent text-white hover:bg-white/10 shadow-none",
|
||||
outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50 shadow-none",
|
||||
link: "text-primary-500 hover:text-primary-600 hover:underline shadow-none px-0",
|
||||
},
|
||||
@@ -138,15 +139,18 @@ export function Button({
|
||||
type = "button",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
// 确保 size 有默认值
|
||||
const actualSize = size ?? "md";
|
||||
|
||||
// 计算样式
|
||||
const computedClass = cn(
|
||||
buttonVariants({ variant, size, fullWidth }),
|
||||
buttonVariants({ variant, size: actualSize, fullWidth }),
|
||||
selected && variant === "secondary" && "bg-gray-200",
|
||||
className
|
||||
);
|
||||
|
||||
// 图标尺寸映射
|
||||
const iconSize = { sm: 14, md: 16, lg: 20 }[size];
|
||||
const iconSize = { sm: 14, md: 16, lg: 20 }[actualSize];
|
||||
|
||||
// 渲染 SVG 图标
|
||||
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
|
||||
@@ -248,6 +252,9 @@ export const SecondaryButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="secondary" {...props} />
|
||||
);
|
||||
|
||||
// LightButton: 次要按钮的别名(向后兼容)
|
||||
export const LightButton = SecondaryButton;
|
||||
|
||||
export const SuccessButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="success" {...props} />
|
||||
);
|
||||
@@ -264,6 +271,11 @@ export const GhostButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="ghost" {...props} />
|
||||
);
|
||||
|
||||
// GhostLightButton: 透明按钮(白色文字,用于深色背景)
|
||||
export const GhostLightButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="ghost-light" {...props} />
|
||||
);
|
||||
|
||||
export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="outline" {...props} />
|
||||
);
|
||||
@@ -271,3 +283,69 @@ export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
export const LinkButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="link" {...props} />
|
||||
);
|
||||
|
||||
// ========== 其他便捷组件 ==========
|
||||
|
||||
// IconButton: SVG 图标按钮(使用 ghost 变体)
|
||||
export const IconButton = (props: Omit<ButtonProps, "variant"> & { icon?: React.ReactNode }) => {
|
||||
const { icon, ...rest } = props;
|
||||
return <Button variant="ghost" leftIcon={icon} {...rest} />;
|
||||
};
|
||||
|
||||
// IconClick: 图片图标按钮(支持 Next.js Image)
|
||||
export const IconClick = (props: Omit<ButtonProps, "variant"> & {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
size?: number | "sm" | "md" | "lg";
|
||||
disableOnHoverBgChange?: boolean;
|
||||
}) => {
|
||||
const { src, alt, size, disableOnHoverBgChange, className, ...rest } = props;
|
||||
let buttonSize: "sm" | "md" | "lg" = "md";
|
||||
if (typeof size === "number") {
|
||||
if (size <= 20) buttonSize = "sm";
|
||||
else if (size >= 32) buttonSize = "lg";
|
||||
} else if (typeof size === "string") {
|
||||
buttonSize = (size === "sm" || size === "md" || size === "lg") ? size : "md";
|
||||
}
|
||||
|
||||
const hoverClass = disableOnHoverBgChange ? "hover:bg-black/30" : "";
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconSrc={src}
|
||||
iconAlt={alt}
|
||||
size={buttonSize}
|
||||
className={`${hoverClass} ${className || ""}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// CircleButton: 圆形图标按钮
|
||||
export const CircleButton = (props: Omit<ButtonProps, "variant"> & { icon?: React.ReactNode }) => {
|
||||
const { icon, className, ...rest } = props;
|
||||
return <Button variant="ghost" leftIcon={icon} className={`rounded-full ${className || ""}`} {...rest} />;
|
||||
};
|
||||
|
||||
// CircleToggleButton: 带选中状态的圆形切换按钮
|
||||
export const CircleToggleButton = (props: Omit<ButtonProps, "variant"> & { selected?: boolean }) => {
|
||||
const { selected, className, children, ...rest } = props;
|
||||
const selectedClass = selected
|
||||
? "bg-primary-500 text-white"
|
||||
: "bg-gray-200 text-gray-600 hover:bg-gray-300";
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={`rounded-full px-3 py-1 text-sm transition-colors ${selectedClass} ${className || ""}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
// DashedButton: 虚线边框按钮(使用 outline 变体近似)
|
||||
export const DashedButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="outline" className="border-dashed" {...props} />
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
const cardVariants = cva(
|
||||
// 基础样式
|
||||
"rounded-2xl bg-white transition-all duration-250",
|
||||
"rounded-lg bg-white transition-all duration-250",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -34,11 +34,11 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
const inputVariants = cva(
|
||||
// 基础样式
|
||||
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 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",
|
||||
"flex w-full rounded-md border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
|
||||
default: "border-b-2 border-gray-300 bg-transparent rounded-t-md",
|
||||
bordered: "border-gray-300 bg-white",
|
||||
filled: "border-transparent bg-gray-100",
|
||||
search: "border-gray-200 bg-white pl-10 rounded-full",
|
||||
|
||||
@@ -186,12 +186,13 @@ export function RadioGroup({
|
||||
// 为每个 Radio 注入 name 和 onChange
|
||||
const enhancedChildren = React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return React.cloneElement(child, {
|
||||
const childProps = child.props as { value?: string; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void };
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
name,
|
||||
checked: value !== undefined ? child.props.value === value : undefined,
|
||||
checked: value !== undefined ? childProps.value === value : undefined,
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value);
|
||||
child.props.onChange?.(e);
|
||||
childProps.onChange?.(e);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
const selectVariants = cva(
|
||||
// 基础样式
|
||||
"flex w-full appearance-none items-center justify-between rounded-xl border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 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",
|
||||
"flex w-full appearance-none items-center justify-between rounded-md border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
|
||||
default: "border-b-2 border-gray-300 bg-transparent rounded-t-md",
|
||||
bordered: "border-gray-300 bg-white",
|
||||
filled: "border-transparent bg-gray-100",
|
||||
},
|
||||
|
||||
@@ -104,19 +104,22 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
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",
|
||||
}[size];
|
||||
}[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",
|
||||
}[size];
|
||||
}[actualSize];
|
||||
|
||||
const renderSwitch = () => (
|
||||
<div className="relative inline-block">
|
||||
|
||||
@@ -27,7 +27,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
const textareaVariants = cva(
|
||||
// 基础样式
|
||||
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 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 resize-none",
|
||||
"flex w-full rounded-md border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 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 resize-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -98,13 +98,16 @@ export function Badge({
|
||||
info: "bg-info-500",
|
||||
};
|
||||
|
||||
// 确保 variant 有默认值
|
||||
const actualVariant = variant ?? "default";
|
||||
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant, size, dot }), className)} {...props}>
|
||||
<div className={cn(badgeVariants({ variant: actualVariant, size, dot }), className)} {...props}>
|
||||
{dot && (
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
dotColor || dotColors[variant]
|
||||
dotColor || dotColors[actualVariant]
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function Divider({
|
||||
<div
|
||||
className={cn(dividerVariants({ variant, orientation }), className)}
|
||||
role="separator"
|
||||
aria-orientation={orientation}
|
||||
aria-orientation={orientation as "horizontal" | "vertical"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
const alertVariants = cva(
|
||||
// 基础样式
|
||||
"rounded-xl border-2 px-4 py-3 shadow-sm transition-all duration-250",
|
||||
"rounded-lg border-2 px-4 py-3 shadow-sm transition-all duration-250",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -127,6 +127,9 @@ export function Alert({
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
// 确保 variant 有默认值
|
||||
const actualVariant = variant ?? "info";
|
||||
|
||||
// 图标颜色
|
||||
const iconColors = {
|
||||
info: "text-info-500",
|
||||
@@ -137,14 +140,14 @@ export function Alert({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
className={cn(alertVariants({ variant: actualVariant }), className)}
|
||||
role="alert"
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* 图标 */}
|
||||
<div className={cn("shrink-0", iconColors[variant])}>
|
||||
{icon || defaultIcons[variant]}
|
||||
<div className={cn("shrink-0", iconColors[actualVariant])}>
|
||||
{icon || defaultIcons[actualVariant]}
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
|
||||
@@ -102,7 +102,8 @@ export function Progress({
|
||||
warning: "bg-warning-500",
|
||||
error: "bg-error-500",
|
||||
};
|
||||
return colors[variant];
|
||||
const actualVariant = variant ?? "default";
|
||||
return colors[actualVariant];
|
||||
};
|
||||
|
||||
// 格式化标签
|
||||
@@ -175,7 +176,7 @@ export function CircularProgress({
|
||||
warning: "#f59e0b",
|
||||
error: "#ef4444",
|
||||
};
|
||||
const strokeColor = colors[variant];
|
||||
const strokeColor = colors[variant ?? "default"];
|
||||
|
||||
return (
|
||||
<div className={cn("inline-flex items-center justify-center", className)}>
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonE
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
variant?: "line" | "enclosed" | "soft";
|
||||
"data-state"?: string;
|
||||
}
|
||||
|
||||
const triggerVariants = cva(
|
||||
@@ -148,14 +149,15 @@ export function TabsTrigger({
|
||||
children,
|
||||
variant = "line",
|
||||
className,
|
||||
"data-state": dataState,
|
||||
...props
|
||||
}: TabsTriggerProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={props["data-state"] === "active"}
|
||||
data-state={props["data-state"]}
|
||||
aria-selected={dataState === "active"}
|
||||
data-state={dataState}
|
||||
className={cn(triggerVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
@@ -170,21 +172,24 @@ export function TabsTrigger({
|
||||
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
"data-state"?: string;
|
||||
}
|
||||
|
||||
export function TabsContent({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
"data-state": dataState,
|
||||
...props
|
||||
}: TabsContentProps) {
|
||||
if (value !== props["data-state"]) return null;
|
||||
if (value !== dataState) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
className={cn("mt-4 focus:outline-none", className)}
|
||||
tabIndex={0}
|
||||
data-state={dataState}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function Modal({
|
||||
{/* 模态框内容 */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-10 w-full bg-white rounded-2xl shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
|
||||
"relative z-10 w-full bg-white rounded-lg shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user