buster/web/src/components/ui/buttons/AppButton.tsx

157 lines
4.3 KiB
TypeScript
Raw Normal View History

2025-02-23 04:50:10 +08:00
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
2025-02-23 05:01:55 +08:00
import { cn } from '@/lib/classMerge';
2025-02-23 05:58:58 +08:00
import { CircleSpinnerLoader } from '../loaders/CircleSpinnerLoader';
2025-02-23 05:01:55 +08:00
const buttonVariants = cva(
2025-02-23 07:19:43 +08:00
'inline-flex items-center overflow-hidden justify-center gap-1.5 shadow-btn rounded transition-all duration-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none cursor-pointer disabled:cursor-not-allowed data-[loading=true]:cursor-progress',
2025-02-23 05:01:55 +08:00
{
variants: {
2025-02-23 05:58:58 +08:00
buttonType: {
default:
2025-02-23 06:20:50 +08:00
'bg-white border hover:bg-item-hover disabled:bg-disabled disabled:text-light-gray active:bg-item-active data-[selected=true]:bg-nav-background',
2025-02-23 06:05:27 +08:00
black: 'bg-black text-white hover:bg-black-hover disabled:bg-black-hover',
2025-02-23 06:27:32 +08:00
primary:
'bg-primary text-white hover:bg-primary-light active:bg-primary-dark data-[selected=true]:bg-primary-dark',
ghost:
'bg-transparent text-dark-gray shadow-none hover:bg-item-hover disabled:bg-transparent disabled:text-light-gray active:bg-item-active data-[selected=true]:bg-nav-background'
2025-02-23 05:01:55 +08:00
},
size: {
default: 'h-6',
2025-02-23 07:07:58 +08:00
tall: 'h-7',
small: 'h-[18px]'
},
iconButton: {
true: '',
false: 'px-2.5'
2025-02-23 05:01:55 +08:00
}
},
2025-02-23 07:07:58 +08:00
compoundVariants: [
{
iconButton: true,
size: 'default',
className: 'w-6'
},
{
iconButton: true,
size: 'tall',
className: 'w-7'
},
{
iconButton: true,
size: 'small',
className: 'w-[18px]'
}
],
2025-02-23 05:01:55 +08:00
defaultVariants: {
2025-02-23 05:58:58 +08:00
buttonType: 'default',
2025-02-23 07:07:58 +08:00
size: 'default',
iconButton: false
2025-02-23 05:01:55 +08:00
}
}
);
2025-02-23 07:19:43 +08:00
const iconVariants = cva('', {
variants: {
buttonType: {
default: 'text-icon',
black: 'text-white',
primary: 'text-white',
ghost: 'text-icon'
},
size: {
default: 'text-icon-size',
tall: 'text-icon-size',
small: 'text-icon-size-small'
}
}
});
2025-02-23 05:01:55 +08:00
export interface ButtonProps
2025-02-23 05:58:58 +08:00
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'prefix'>,
2025-02-23 05:01:55 +08:00
VariantProps<typeof buttonVariants> {
asChild?: boolean;
2025-02-23 05:58:58 +08:00
prefix?: React.ReactNode;
suffix?: React.ReactNode;
loading?: boolean;
selected?: boolean;
2025-02-23 07:09:47 +08:00
prefixClassName?: string;
suffixClassName?: string;
2025-02-23 05:01:55 +08:00
}
const AppButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
2025-02-23 05:58:58 +08:00
(
{
2025-02-23 06:20:50 +08:00
className = '',
2025-02-23 06:05:27 +08:00
buttonType = 'default',
2025-02-23 06:20:50 +08:00
size = 'default',
2025-02-23 05:58:58 +08:00
asChild = false,
prefix,
suffix,
children,
loading = false,
selected = false,
2025-02-23 06:20:50 +08:00
disabled = false,
2025-02-23 07:09:47 +08:00
prefixClassName,
suffixClassName,
2025-02-23 05:58:58 +08:00
...props
},
ref
) => {
2025-02-23 05:01:55 +08:00
const Comp = asChild ? Slot : 'button';
2025-02-23 07:07:58 +08:00
const hasChildren = !!children;
const iconButton = !hasChildren && (!prefix || !suffix);
2025-02-23 05:01:55 +08:00
return (
2025-02-23 05:58:58 +08:00
<Comp
2025-02-23 07:07:58 +08:00
className={cn(buttonVariants({ buttonType, size, iconButton, className }))}
2025-02-23 05:58:58 +08:00
ref={ref}
2025-02-23 06:20:50 +08:00
disabled={disabled}
2025-02-23 05:58:58 +08:00
data-loading={loading}
data-selected={selected}
{...props}>
2025-02-23 07:07:58 +08:00
{loading ? (
<LoadingIcon buttonType={buttonType} />
) : (
prefix && (
2025-02-23 07:19:43 +08:00
<span className={cn(iconVariants({ buttonType, size }), prefixClassName)}>
2025-02-23 07:09:47 +08:00
{prefix}
</span>
2025-02-23 07:07:58 +08:00
)
)}
{hasChildren && <span className="text-base">{children}</span>}
{suffix && (
2025-02-23 07:19:43 +08:00
<span className={cn(iconVariants({ buttonType, size }), suffixClassName)}>{suffix}</span>
2025-02-23 07:07:58 +08:00
)}
2025-02-23 05:58:58 +08:00
</Comp>
2025-02-23 05:01:55 +08:00
);
}
);
AppButton.displayName = 'AppButton';
2025-02-23 06:05:27 +08:00
const LoadingIcon: React.FC<{
2025-02-23 06:27:32 +08:00
buttonType: 'default' | 'black' | 'primary' | 'ghost' | null;
2025-02-23 06:05:27 +08:00
}> = ({ buttonType = 'default' }) => {
return (
<div
className={cn(
2025-02-23 07:07:58 +08:00
'flex h-[15px] w-[1em] items-center justify-center text-black dark:text-white',
2025-02-23 06:05:27 +08:00
buttonType === 'black' && 'dark'
)}>
<CircleSpinnerLoader
2025-02-23 07:07:58 +08:00
size={9.5}
2025-02-23 06:27:32 +08:00
fill={
buttonType === 'black'
? 'var(--color-white)'
: buttonType === 'primary'
? 'var(--color-primary)'
: 'var(--color-primary)'
}
2025-02-23 06:05:27 +08:00
/>
</div>
);
};
2025-02-23 05:01:55 +08:00
export { AppButton, buttonVariants };