fix: refactor

This commit is contained in:
shinya
2025-06-19 23:03:37 +08:00
parent 29f729df54
commit 8e4781f44c
23 changed files with 221 additions and 1513 deletions

View File

@@ -1,160 +0,0 @@
import { LucideIcon } from 'lucide-react';
import * as React from 'react';
import { IconType } from 'react-icons';
import { ImSpinner2 } from 'react-icons/im';
import { cn } from '@/lib/utils';
const ButtonVariant = ['primary', 'outline', 'ghost', 'light', 'dark'] as const;
const ButtonSize = ['sm', 'base'] as const;
type ButtonProps = {
isLoading?: boolean;
isDarkBg?: boolean;
variant?: (typeof ButtonVariant)[number];
size?: (typeof ButtonSize)[number];
leftIcon?: IconType | LucideIcon;
rightIcon?: IconType | LucideIcon;
classNames?: {
leftIcon?: string;
rightIcon?: string;
};
} & React.ComponentPropsWithRef<'button'>;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
className,
disabled: buttonDisabled,
isLoading,
variant = 'primary',
size = 'base',
isDarkBg = false,
leftIcon: LeftIcon,
rightIcon: RightIcon,
classNames,
...rest
},
ref
) => {
const disabled = isLoading || buttonDisabled;
return (
<button
ref={ref}
type='button'
disabled={disabled}
className={cn(
'inline-flex items-center rounded font-medium',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
'shadow-sm',
'transition-colors duration-75',
//#region //*=========== Size ===========
[
size === 'base' && ['px-3 py-1.5', 'text-sm md:text-base'],
size === 'sm' && ['px-2 py-1', 'text-xs md:text-sm'],
],
//#endregion //*======== Size ===========
//#region //*=========== Variants ===========
[
variant === 'primary' && [
'bg-primary-500 text-white',
'border-primary-600 border',
'hover:bg-primary-600 hover:text-white',
'active:bg-primary-700',
'disabled:bg-primary-700',
],
variant === 'outline' && [
'text-primary-500',
'border-primary-500 border',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'ghost' && [
'text-primary-500',
'shadow-none',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'light' && [
'bg-white text-gray-700',
'border border-gray-300',
'hover:text-dark hover:bg-gray-100',
'active:bg-white/80 disabled:bg-gray-200',
],
variant === 'dark' && [
'bg-gray-900 text-white',
'border border-gray-600',
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
],
],
//#endregion //*======== Variants ===========
'disabled:cursor-not-allowed',
isLoading &&
'relative text-transparent transition-none hover:text-transparent disabled:cursor-wait',
className
)}
{...rest}
>
{isLoading && (
<div
className={cn(
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
{
'text-white': ['primary', 'dark'].includes(variant),
'text-black': ['light'].includes(variant),
'text-primary-500': ['outline', 'ghost'].includes(variant),
}
)}
>
<ImSpinner2 className='animate-spin' />
</div>
)}
{LeftIcon && (
<div
className={cn([
size === 'base' && 'mr-1',
size === 'sm' && 'mr-1.5',
])}
>
<LeftIcon
size='1em'
className={cn(
[
size === 'base' && 'md:text-md text-md',
size === 'sm' && 'md:text-md text-sm',
],
classNames?.leftIcon
)}
/>
</div>
)}
{children}
{RightIcon && (
<div
className={cn([
size === 'base' && 'ml-1',
size === 'sm' && 'ml-1.5',
])}
>
<RightIcon
size='1em'
className={cn(
[
size === 'base' && 'text-md md:text-md',
size === 'sm' && 'md:text-md text-sm',
],
classNames?.rightIcon
)}
/>
</div>
)}
</button>
);
}
);
export default Button;

View File

@@ -1,116 +0,0 @@
import { LucideIcon } from 'lucide-react';
import * as React from 'react';
import { IconType } from 'react-icons';
import { ImSpinner2 } from 'react-icons/im';
import { cn } from '@/lib/utils';
const IconButtonVariant = [
'primary',
'outline',
'ghost',
'light',
'dark',
] as const;
type IconButtonProps = {
isLoading?: boolean;
isDarkBg?: boolean;
variant?: (typeof IconButtonVariant)[number];
icon?: IconType | LucideIcon;
classNames?: {
icon?: string;
};
} & React.ComponentPropsWithRef<'button'>;
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
className,
disabled: buttonDisabled,
isLoading,
variant = 'primary',
isDarkBg = false,
icon: Icon,
classNames,
...rest
},
ref
) => {
const disabled = isLoading || buttonDisabled;
return (
<button
ref={ref}
type='button'
disabled={disabled}
className={cn(
'inline-flex items-center justify-center rounded font-medium',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
'shadow-sm',
'transition-colors duration-75',
'min-h-[28px] min-w-[28px] p-1 md:min-h-[34px] md:min-w-[34px] md:p-2',
//#region //*=========== Variants ===========
[
variant === 'primary' && [
'bg-primary-500 text-white',
'border-primary-600 border',
'hover:bg-primary-600 hover:text-white',
'active:bg-primary-700',
'disabled:bg-primary-700',
],
variant === 'outline' && [
'text-primary-500',
'border-primary-500 border',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'ghost' && [
'text-primary-500',
'shadow-none',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'light' && [
'bg-white text-gray-700',
'border border-gray-300',
'hover:text-dark hover:bg-gray-100',
'active:bg-white/80 disabled:bg-gray-200',
],
variant === 'dark' && [
'bg-gray-900 text-white',
'border border-gray-600',
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
],
],
//#endregion //*======== Variants ===========
'disabled:cursor-not-allowed',
isLoading &&
'relative text-transparent transition-none hover:text-transparent disabled:cursor-wait',
className
)}
{...rest}
>
{isLoading && (
<div
className={cn(
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
{
'text-white': ['primary', 'dark'].includes(variant),
'text-black': ['light'].includes(variant),
'text-primary-500': ['outline', 'ghost'].includes(variant),
}
)}
>
<ImSpinner2 className='animate-spin' />
</div>
)}
{Icon && <Icon size='1em' className={cn(classNames?.icon)} />}
</button>
);
}
);
export default IconButton;

View File

@@ -1,52 +0,0 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const TextButtonVariant = ['primary', 'basic'] as const;
type TextButtonProps = {
variant?: (typeof TextButtonVariant)[number];
} & React.ComponentPropsWithRef<'button'>;
const TextButton = React.forwardRef<HTMLButtonElement, TextButtonProps>(
(
{
children,
className,
variant = 'primary',
disabled: buttonDisabled,
...rest
},
ref
) => {
return (
<button
ref={ref}
type='button'
disabled={buttonDisabled}
className={cn(
'button inline-flex items-center justify-center font-semibold',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
'transition duration-100',
//#region //*=========== Variant ===========
variant === 'primary' && [
'text-primary-500 hover:text-primary-600 active:text-primary-700',
'disabled:text-primary-200',
],
variant === 'basic' && [
'text-black hover:text-gray-600 active:text-gray-800',
'disabled:text-gray-300',
],
//#endregion //*======== Variant ===========
'disabled:cursor-not-allowed disabled:brightness-105 disabled:hover:underline',
className
)}
{...rest}
>
{children}
</button>
);
}
);
export default TextButton;

View File

@@ -1,64 +0,0 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
import UnderlineLink from '@/components/links/UnderlineLink';
import { UnstyledLinkProps } from '@/components/links/UnstyledLink';
type ArrowLinkProps<C extends React.ElementType> = {
as?: C;
direction?: 'left' | 'right';
} & UnstyledLinkProps &
React.ComponentProps<C>;
export default function ArrowLink<C extends React.ElementType>({
children,
className,
direction = 'right',
as,
...rest
}: ArrowLinkProps<C>) {
const Component = as || UnderlineLink;
return (
<Component
{...rest}
className={cn(
'group gap-[0.25em]',
direction === 'left' && 'flex-row-reverse',
className
)}
>
<span>{children}</span>
<svg
viewBox='0 0 16 16'
height='1em'
width='1em'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={cn(
'relative',
'transition-transform duration-200',
direction === 'right' ? 'motion-safe:-translate-x-1' : 'rotate-180',
'group-hover:translate-x-0'
)}
>
<path
fill='currentColor'
d='M7.28033 3.21967C6.98744 2.92678 6.51256 2.92678 6.21967 3.21967C5.92678 3.51256 5.92678 3.98744 6.21967 4.28033L7.28033 3.21967ZM11 8L11.5303 8.53033C11.8232 8.23744 11.8232 7.76256 11.5303 7.46967L11 8ZM6.21967 11.7197C5.92678 12.0126 5.92678 12.4874 6.21967 12.7803C6.51256 13.0732 6.98744 13.0732 7.28033 12.7803L6.21967 11.7197ZM6.21967 4.28033L10.4697 8.53033L11.5303 7.46967L7.28033 3.21967L6.21967 4.28033ZM10.4697 7.46967L6.21967 11.7197L7.28033 12.7803L11.5303 8.53033L10.4697 7.46967Z'
/>
<path
stroke='currentColor'
d='M1.75 8H11'
strokeWidth='1.5'
strokeLinecap='round'
className={cn(
'origin-left transition-all duration-200',
'opacity-0 motion-safe:-translate-x-1',
'group-hover:translate-x-0 group-hover:opacity-100'
)}
/>
</svg>
</Component>
);
}

View File

@@ -1,146 +0,0 @@
import { LucideIcon } from 'lucide-react';
import * as React from 'react';
import { IconType } from 'react-icons';
import { cn } from '@/lib/utils';
import UnstyledLink, {
UnstyledLinkProps,
} from '@/components/links/UnstyledLink';
const ButtonLinkVariant = [
'primary',
'outline',
'ghost',
'light',
'dark',
] as const;
const ButtonLinkSize = ['sm', 'base'] as const;
type ButtonLinkProps = {
isDarkBg?: boolean;
variant?: (typeof ButtonLinkVariant)[number];
size?: (typeof ButtonLinkSize)[number];
leftIcon?: IconType | LucideIcon;
rightIcon?: IconType | LucideIcon;
classNames?: {
leftIcon?: string;
rightIcon?: string;
};
} & UnstyledLinkProps;
const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
(
{
children,
className,
variant = 'primary',
size = 'base',
isDarkBg = false,
leftIcon: LeftIcon,
rightIcon: RightIcon,
classNames,
...rest
},
ref
) => {
return (
<UnstyledLink
ref={ref}
{...rest}
className={cn(
'inline-flex items-center rounded font-medium',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
'shadow-sm',
'transition-colors duration-75',
//#region //*=========== Size ===========
[
size === 'base' && ['px-3 py-1.5', 'text-sm md:text-base'],
size === 'sm' && ['px-2 py-1', 'text-xs md:text-sm'],
],
//#endregion //*======== Size ===========
//#region //*=========== Variants ===========
[
variant === 'primary' && [
'bg-primary-500 text-white',
'border-primary-600 border',
'hover:bg-primary-600 hover:text-white',
'active:bg-primary-700',
'disabled:bg-primary-700',
],
variant === 'outline' && [
'text-primary-500',
'border-primary-500 border',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'ghost' && [
'text-primary-500',
'shadow-none',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'light' && [
'bg-white text-gray-700',
'border border-gray-300',
'hover:text-dark hover:bg-gray-100',
'active:bg-white/80 disabled:bg-gray-200',
],
variant === 'dark' && [
'bg-gray-900 text-white',
'border border-gray-600',
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
],
],
//#endregion //*======== Variants ===========
'disabled:cursor-not-allowed',
className
)}
>
{LeftIcon && (
<div
className={cn([
size === 'base' && 'mr-1',
size === 'sm' && 'mr-1.5',
])}
>
<LeftIcon
size='1em'
className={cn(
[
size === 'base' && 'md:text-md text-md',
size === 'sm' && 'md:text-md text-sm',
],
classNames?.leftIcon
)}
/>
</div>
)}
{children}
{RightIcon && (
<div
className={cn([
size === 'base' && 'ml-1',
size === 'sm' && 'ml-1.5',
])}
>
<RightIcon
size='1em'
className={cn(
[
size === 'base' && 'text-md md:text-md',
size === 'sm' && 'md:text-md text-sm',
],
classNames?.rightIcon
)}
/>
</div>
)}
</UnstyledLink>
);
}
);
export default ButtonLink;

View File

@@ -1,97 +0,0 @@
import { LucideIcon } from 'lucide-react';
import * as React from 'react';
import { IconType } from 'react-icons';
import { cn } from '@/lib/utils';
import UnstyledLink, {
UnstyledLinkProps,
} from '@/components/links/UnstyledLink';
const IconLinkVariant = [
'primary',
'outline',
'ghost',
'light',
'dark',
] as const;
type IconLinkProps = {
isDarkBg?: boolean;
variant?: (typeof IconLinkVariant)[number];
icon?: IconType | LucideIcon;
classNames?: {
icon?: string;
};
} & Omit<UnstyledLinkProps, 'children'>;
const IconLink = React.forwardRef<HTMLAnchorElement, IconLinkProps>(
(
{
className,
icon: Icon,
variant = 'outline',
isDarkBg = false,
classNames,
...rest
},
ref
) => {
return (
<UnstyledLink
ref={ref}
type='button'
className={cn(
'inline-flex items-center justify-center rounded font-medium',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
'shadow-sm',
'transition-colors duration-75',
'min-h-[28px] min-w-[28px] p-1 md:min-h-[34px] md:min-w-[34px] md:p-2',
//#region //*=========== Variants ===========
[
variant === 'primary' && [
'bg-primary-500 text-white',
'border-primary-600 border',
'hover:bg-primary-600 hover:text-white',
'active:bg-primary-700',
'disabled:bg-primary-700',
],
variant === 'outline' && [
'text-primary-500',
'border-primary-500 border',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'ghost' && [
'text-primary-500',
'shadow-none',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'light' && [
'bg-white text-gray-700',
'border border-gray-300',
'hover:text-dark hover:bg-gray-100',
'active:bg-white/80 disabled:bg-gray-200',
],
variant === 'dark' && [
'bg-gray-900 text-white',
'border border-gray-600',
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
],
],
//#endregion //*======== Variants ===========
'disabled:cursor-not-allowed',
className
)}
{...rest}
>
{Icon && <Icon size='1em' className={cn(classNames?.icon)} />}
</UnstyledLink>
);
}
);
export default IconLink;

View File

@@ -1,43 +0,0 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
import UnstyledLink, {
UnstyledLinkProps,
} from '@/components/links/UnstyledLink';
const PrimaryLinkVariant = ['primary', 'basic'] as const;
type PrimaryLinkProps = {
variant?: (typeof PrimaryLinkVariant)[number];
} & UnstyledLinkProps;
const PrimaryLink = React.forwardRef<HTMLAnchorElement, PrimaryLinkProps>(
({ className, children, variant = 'primary', ...rest }, ref) => {
return (
<UnstyledLink
ref={ref}
{...rest}
className={cn(
'inline-flex items-center',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:rounded focus-visible:ring focus-visible:ring-offset-2',
'font-medium',
//#region //*=========== Variant ===========
variant === 'primary' && [
'text-primary-500 hover:text-primary-600 active:text-primary-700',
'disabled:text-primary-200',
],
variant === 'basic' && [
'text-black hover:text-gray-600 active:text-gray-800',
'disabled:text-gray-300',
],
//#endregion //*======== Variant ===========
className
)}
>
{children}
</UnstyledLink>
);
}
);
export default PrimaryLink;

View File

@@ -1,28 +0,0 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
import UnstyledLink, {
UnstyledLinkProps,
} from '@/components/links/UnstyledLink';
const UnderlineLink = React.forwardRef<HTMLAnchorElement, UnstyledLinkProps>(
({ children, className, ...rest }, ref) => {
return (
<UnstyledLink
ref={ref}
{...rest}
className={cn(
'animated-underline custom-link inline-flex items-center font-medium',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:rounded focus-visible:ring focus-visible:ring-offset-2',
'border-dark border-b border-dotted hover:border-black/0',
className
)}
>
{children}
</UnstyledLink>
);
}
);
export default UnderlineLink;

View File

@@ -1,50 +0,0 @@
import Link, { LinkProps } from 'next/link';
import * as React from 'react';
import { cn } from '@/lib/utils';
export type UnstyledLinkProps = {
href: string;
children: React.ReactNode;
openNewTab?: boolean;
className?: string;
nextLinkProps?: Omit<LinkProps, 'href'>;
} & React.ComponentPropsWithRef<'a'>;
const UnstyledLink = React.forwardRef<HTMLAnchorElement, UnstyledLinkProps>(
({ children, href, openNewTab, className, nextLinkProps, ...rest }, ref) => {
const isNewTab =
openNewTab !== undefined
? openNewTab
: href && !href.startsWith('/') && !href.startsWith('#');
if (!isNewTab) {
return (
<Link
href={href}
ref={ref}
className={className}
{...rest}
{...nextLinkProps}
>
{children}
</Link>
);
}
return (
<a
ref={ref}
target='_blank'
rel='noopener noreferrer'
href={href}
{...rest}
className={cn('cursor-newtab', className)}
>
{children}
</a>
);
}
);
export default UnstyledLink;