Files
kycnotme/web/src/components/Button.astro
2025-06-11 22:49:54 +00:00

305 lines
6.9 KiB
Plaintext

---
import { Icon } from 'astro-icon/components'
import { tv, type VariantProps } from 'tailwind-variants'
import { cn } from '../lib/cn'
import type { HTMLAttributes, Polymorphic } from 'astro/types'
type Props<Tag extends 'a' | 'button' | 'label' | 'span' = 'button'> = Polymorphic<
Required<Pick<HTMLAttributes<'label'>, Tag extends 'label' ? 'for' : never>> &
VariantProps<typeof button> & {
as: Tag
label: string
icon?: string
endIcon?: string
classNames?: {
label?: string
icon?: string
endIcon?: string
}
dataAstroReload?: boolean
children?: never
disabled?: boolean
inlineIcon?: boolean
}
>
export type ButtonProps<Tag extends 'a' | 'button' | 'label' = 'button'> = Props<Tag>
const button = tv({
slots: {
base: 'inline-flex shrink-0 items-center justify-center gap-2 rounded-lg border transition-colors duration-100 focus-visible:ring-2 focus-visible:ring-current focus-visible:ring-offset-2 focus-visible:ring-offset-black focus-visible:outline-hidden',
icon: 'size-4 shrink-0',
label: 'text-left whitespace-nowrap',
endIcon: 'size-4 shrink-0',
},
variants: {
size: {
sm: {
base: 'h-8 px-3 text-sm',
icon: 'size-4',
endIcon: 'size-4',
},
md: {
base: 'h-9 px-4 text-sm',
icon: 'size-4',
endIcon: 'size-4',
label: 'font-medium',
},
lg: {
base: 'h-10 px-5 text-base',
icon: 'size-5',
endIcon: 'size-5',
label: 'font-bold tracking-wider uppercase',
},
},
iconOnly: {
true: {
base: 'p-0',
label: 'sr-only',
},
},
color: {
black: '',
white: '',
gray: '',
success: '',
danger: '',
warning: '',
info: '',
},
variant: {
solid: '',
faded: '',
},
shadow: {
true: {
base: 'shadow-lg',
},
},
disabled: {
true: {
base: 'cursor-not-allowed',
},
},
},
compoundVariants: [
// Color variants - solid
{
color: 'black',
variant: 'solid',
class: {
base: 'border-night-500 bg-night-800 hover:bg-night-900 hover:text-day-200 focus-visible:bg-night-500 text-white/50 focus-visible:text-white focus-visible:ring-white',
},
},
{
color: 'white',
variant: 'solid',
class: {
base: 'border-day-300 bg-day-100 hover:bg-day-200 text-black focus-visible:ring-green-500',
},
},
{
color: 'gray',
variant: 'solid',
class: {
base: 'border-day-500 bg-day-400 hover:bg-day-500 text-black focus-visible:ring-white',
},
},
{
color: 'success',
variant: 'solid',
class: {
base: 'border-green-600 bg-green-500 text-black hover:bg-green-600',
},
},
{
color: 'danger',
variant: 'solid',
class: {
base: 'border-red-600 bg-red-500 text-white hover:bg-red-600',
},
},
{
color: 'warning',
variant: 'solid',
class: {
base: 'border-yellow-600 bg-yellow-500 text-white hover:bg-yellow-600',
},
},
{
color: 'info',
variant: 'solid',
class: {
base: 'border-blue-600 bg-blue-500 text-white hover:bg-blue-600',
},
},
// Color variants - faded
{
color: 'black',
variant: 'faded',
class: {
base: 'bg2night-800/15 hover:bg-night-700/30 border-current/30 text-white/70 hover:text-white/90 focus-visible:ring-white/50',
},
},
{
color: 'white',
variant: 'faded',
class: {
base: 'b2-day-100/15 hover:bg-day-200/30 border-current/30 text-white/70 hover:text-white/90 focus-visible:ring-white/50',
},
},
{
color: 'gray',
variant: 'faded',
class: {
base: 'b2-day-400/15 hover:bg-day-500/30 text-day-300 hover:text-day-100 border-current/30 focus-visible:ring-white/50',
},
},
{
color: 'success',
variant: 'faded',
class: {
base: 'border-current/20 bg-green-500/15 text-green-300 hover:bg-green-500/30 hover:text-green-100',
},
},
{
color: 'danger',
variant: 'faded',
class: {
base: 'border-current/20 bg-red-500/15 text-red-300 hover:bg-red-500/30 hover:text-red-100',
},
},
{
color: 'warning',
variant: 'faded',
class: {
base: 'border-current/20 bg-yellow-500/15 text-yellow-300 hover:bg-yellow-500/30 hover:text-yellow-100',
},
},
{
color: 'info',
variant: 'faded',
class: {
base: 'border-current/20 bg-blue-500/15 text-blue-300 hover:bg-blue-500/30 hover:text-blue-100',
},
},
// Shadow variants
{
color: 'black',
shadow: true,
class: 'shadow-black/30',
},
{
color: 'white',
shadow: true,
class: 'shadow-white/30',
},
{
color: 'gray',
shadow: true,
class: 'shadow-day-500/30',
},
{
color: 'success',
shadow: true,
class: 'shadow-green-500/30',
},
{
color: 'danger',
shadow: true,
class: 'shadow-red-500/30',
},
{
color: 'warning',
shadow: true,
class: 'shadow-yellow-500/30',
},
{
color: 'info',
shadow: true,
class: 'shadow-blue-500/30',
},
// Icon only variants
{
iconOnly: true,
size: 'sm',
class: 'w-8',
},
{
iconOnly: true,
size: 'md',
class: 'w-9',
},
{
iconOnly: true,
size: 'lg',
class: 'w-10',
},
],
defaultVariants: {
size: 'md',
color: 'black',
variant: 'solid',
shadow: false,
disabled: false,
iconOnly: false,
},
})
const {
as: Tag = 'button' as 'a' | 'button' | 'label' | 'span',
label,
icon,
endIcon,
size,
color,
variant,
shadow,
class: className,
classNames,
role,
dataAstroReload,
disabled,
inlineIcon,
iconOnly,
...htmlProps
} = Astro.props
const {
base,
icon: iconSlot,
label: labelSlot,
endIcon: endIconSlot,
} = button({
size,
color,
variant,
shadow,
disabled,
iconOnly: iconOnly ?? (!label && !(!!icon && !!endIcon)),
})
const ActualTag = disabled && Tag === 'a' ? 'span' : Tag
---
<ActualTag
class={base({ class: cn({ 'opacity-20 hover:opacity-50': disabled }, className) })}
role={role ?? (Tag === 'button' || Tag === 'label' || (disabled && Tag === 'a') ? undefined : 'button')}
aria-disabled={disabled}
aria-label={label}
{...dataAstroReload && { 'data-astro-reload': dataAstroReload }}
{...htmlProps}
>
{!!icon && <Icon name={icon} class={iconSlot({ class: classNames?.icon })} is:inline={inlineIcon} />}
<span class={labelSlot({ class: classNames?.label })}>{label}</span>
{
!!endIcon && (
<Icon name={endIcon} class={endIconSlot({ class: classNames?.endIcon })} is:inline={inlineIcon}>
{endIcon}
</Icon>
)
}
</ActualTag>