158 lines
9.7 KiB
TypeScript
158 lines
9.7 KiB
TypeScript
'use client'
|
|
|
|
import { ReactNode, forwardRef, useState, useRef, useEffect, ButtonHTMLAttributes } from 'react'
|
|
|
|
type ButtonProps = {
|
|
title?: string
|
|
children?: ReactNode
|
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
|
onToggle?: (open: boolean) => void
|
|
modalId?: string
|
|
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
|
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
|
|
size?: 'xs' |'sm' | 'md' | 'lg'
|
|
className?: string
|
|
dropDirection?: "up" | "down" | "auto"
|
|
disabled?: boolean
|
|
} & ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
{
|
|
title,
|
|
children,
|
|
onClick,
|
|
onToggle,
|
|
modalId,
|
|
color = 'blue',
|
|
variant = 'solid',
|
|
size = 'md',
|
|
className,
|
|
dropDirection = "down",
|
|
disabled = false,
|
|
...rest
|
|
},
|
|
ref
|
|
) {
|
|
const [open, setOpen] = useState(false)
|
|
const [direction, setDirection] = useState<'up' | 'down'>('down')
|
|
const localRef = useRef<HTMLButtonElement>(null)
|
|
const buttonRef = (ref as React.RefObject<HTMLButtonElement>) || localRef
|
|
|
|
const modalAttributes: { [key: string]: string } = modalId
|
|
? {
|
|
'aria-haspopup': 'dialog',
|
|
'aria-expanded': 'false',
|
|
'aria-controls': modalId,
|
|
'data-hs-overlay': `#${modalId}`,
|
|
}
|
|
: {}
|
|
|
|
const sizeClasses: Record<string, string> = {
|
|
xs: 'py-1 px-2',
|
|
sm: 'py-2 px-3',
|
|
md: 'py-3 px-4',
|
|
lg: 'p-4 sm:p-5',
|
|
}
|
|
|
|
const base = `
|
|
${sizeClasses[size] || sizeClasses['md']}
|
|
inline-flex items-center gap-x-2 text-sm font-medium rounded-lg
|
|
focus:outline-hidden disabled:opacity-50 disabled:pointer-events-none
|
|
`
|
|
|
|
const variants: Record<string, Record<string, string>> = {
|
|
solid: {
|
|
blue: 'bg-blue-600 text-white hover:bg-blue-700 focus:bg-blue-700',
|
|
red: 'bg-red-600 text-white hover:bg-red-700 focus:bg-red-700',
|
|
gray: 'bg-gray-600 text-white hover:bg-gray-700 focus:bg-gray-700',
|
|
teal: 'bg-teal-600 text-white hover:bg-teal-700 focus:bg-teal-700',
|
|
green: 'bg-green-600 text-white hover:bg-green-700 focus:bg-green-700',
|
|
transparent: 'bg-transparent-600 text-white hover:bg-transparent-700 focus:bg-transparent-700',
|
|
},
|
|
outline: {
|
|
blue: 'border border-gray-200 text-gray-500 hover:border-blue-600 hover:text-blue-600 focus:border-blue-600 focus:text-blue-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-blue-500 dark:hover:border-blue-600 dark:focus:text-blue-500 dark:focus:border-blue-600',
|
|
red: 'border border-gray-200 text-gray-500 hover:border-red-600 hover:text-red-600 focus:border-red-600 focus:text-red-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-red-500 dark:hover:border-red-600 dark:focus:text-red-500 dark:focus:border-red-600',
|
|
gray: 'border border-gray-200 text-gray-500 hover:border-gray-600 hover:text-gray-600 focus:border-gray-600 focus:text-gray-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
|
|
teal: 'border border-teal-200 text-teal-500 hover:border-teal-600 hover:text-teal-600 focus:border-teal-600 focus:text-teal-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
|
|
green: 'border border-green-200 text-green-500 hover:border-green-600 hover:text-green-600 focus:border-green-600 focus:text-green-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
|
|
transparent: 'border border-transparent-200 text-transparent-500 hover:border-transparent-600 hover:text-transparent-600 focus:border-transparent-600 focus:text-transparent-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
|
|
},
|
|
ghost: {
|
|
blue: 'border border-transparent text-blue-600 hover:bg-blue-100 hover:text-blue-800 focus:bg-blue-100 focus:text-blue-800 dark:text-blue-500 dark:hover:bg-blue-800/30 dark:hover:text-blue-400 dark:focus:bg-blue-800/30 dark:focus:text-blue-400',
|
|
red: 'border border-transparent text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400',
|
|
gray: 'border border-transparent text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
|
teal: 'border border-transparent text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
|
green: 'border border-transparent text-green-600 hover:bg-green-100 hover:text-green-800 focus:bg-green-100 focus:text-green-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
|
transparent: 'border border-transparent text-transparent-600 hover:bg-transparent-100 hover:text-transparent-800 focus:bg-transparent-100 focus:text-transparent-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
|
},
|
|
soft: {
|
|
blue: 'bg-blue-100 text-blue-800 hover:bg-blue-200 focus:bg-blue-200 dark:text-blue-400 dark:hover:bg-blue-900 dark:focus:bg-blue-900',
|
|
red: 'bg-red-100 text-red-800 hover:bg-red-200 focus:bg-red-200 dark:text-red-400 dark:hover:bg-red-900 dark:focus:bg-red-900',
|
|
gray: 'bg-gray-100 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
teal: 'bg-teal-100 text-teal-800 hover:bg-teal-200 focus:bg-teal-200 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
green: 'bg-green-100 text-green-800 hover:bg-green-200 focus:bg-green-200 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
transparent: 'bg-transparent-100 text-transparent-800 hover:bg-transparent-200 focus:bg-transparent-200 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
},
|
|
white: {
|
|
blue: 'border border-teal-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
red: 'border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
gray: 'border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
teal: 'border border-teal-200 bg-white text-teal-800 shadow-2xs hover:bg-teal-50 focus:bg-teal-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
green: 'border border-green-200 bg-white text-green-800 shadow-2xs hover:bg-green-50 focus:bg-green-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
transparent: 'border border-transparent-200 bg-white text-transparent-800 shadow-2xs hover:bg-transparent-50 focus:bg-transparent-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
|
|
},
|
|
link: {
|
|
blue: 'border border-transparent text-blue-600 hover:text-blue-800 focus:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400 dark:focus:text-blue-400',
|
|
red: 'border border-transparent text-red-600 hover:text-red-800 focus:text-red-800 dark:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400',
|
|
gray: 'border border-transparent text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
|
teal: 'border border-transparent text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
|
green: 'border border-transparent text-green-600 hover:text-green-800 focus:text-green-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
|
transparent: 'border border-transparent text-transparent-600 hover:text-transparent-800 focus:text-transparent-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white'
|
|
},
|
|
}
|
|
|
|
const classes = `
|
|
${base}
|
|
${variants[variant]?.[color] || variants.solid.blue}
|
|
${className || ''}
|
|
`
|
|
|
|
useEffect(() => {
|
|
if (open && dropDirection === "auto" && buttonRef.current) {
|
|
requestAnimationFrame(() => {
|
|
const rect = buttonRef.current!.getBoundingClientRect();
|
|
const dropdownHeight = 200;
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
const spaceAbove = rect.top;
|
|
|
|
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
|
|
setDirection("up");
|
|
} else {
|
|
setDirection("down");
|
|
}
|
|
});
|
|
}
|
|
}, [open, dropDirection]);
|
|
|
|
const toggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
const next = !open
|
|
setOpen(next)
|
|
onToggle?.(next)
|
|
onClick?.(event)
|
|
}
|
|
|
|
return (
|
|
<button
|
|
ref={buttonRef}
|
|
type="button"
|
|
className={classes}
|
|
onClick={toggle}
|
|
{...modalAttributes}
|
|
{...rest}
|
|
>
|
|
{children ?? title}
|
|
</button>
|
|
)
|
|
})
|
|
|
|
export default Button |