admin/components/formElements/Button.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

185 lines
5.6 KiB
TypeScript

'use client'
import { Button as HeroButton, Spinner, buttonVariants } from '@heroui/react'
import Link from 'next/link'
import React, { forwardRef } from 'react'
interface ButtonProps {
'aria-controls'?: string
'aria-expanded'?: boolean | 'true' | 'false'
'aria-label'?: string
'aria-pressed'?: boolean | 'true' | 'false'
'aria-checked'?: boolean | 'true' | 'false'
'aria-selected'?: boolean | 'true' | 'false'
children: React.ReactNode
color?: 'default' | 'primary' | 'secondary' | 'secondary-dark' | 'success' | 'warning' | 'danger'
size?: 'sm' | 'md' | 'lg' | 'xl'
variant?: 'solid' | 'bordered' | 'light' | 'flat' | 'faded' | 'shadow' | 'ghost'
type?: 'button' | 'submit' | 'reset'
target?: '_blank' | '_self'
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
role?: React.AriaRole
disabled?: boolean
isLoading?: boolean
iconOnly?: boolean
fullWidth?: boolean
iconStart?: React.ReactNode
iconEnd?: React.ReactNode
className?: string
to?: string
onClick?: () => void
}
type HeroButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost' | 'danger' | 'danger-soft'
function resolveVariant(color: NonNullable<ButtonProps['color']>, variant: ButtonProps['variant']): HeroButtonVariant {
if (color === 'danger') return variant === 'flat' || variant === 'light' ? 'danger-soft' : 'danger'
if (color === 'secondary-dark') return variant === 'flat' || variant === 'light' || variant === 'ghost' ? 'tertiary' : 'primary'
if (variant === 'ghost') return 'ghost'
if (variant === 'bordered' || variant === 'faded') return 'secondary'
if (variant === 'light' || variant === 'flat') return 'tertiary'
return 'primary'
}
function resolveSemanticColorStyle(
color: NonNullable<ButtonProps['color']>,
variant: ButtonProps['variant']
): React.CSSProperties | undefined {
if (color !== 'secondary-dark') return undefined
const isSoft = variant === 'flat' || variant === 'light' || variant === 'ghost'
return {
'--button-bg': isSoft ? 'transparent' : 'var(--color-secondary-900)',
'--button-bg-hover': isSoft ? 'color-mix(in srgb, var(--color-secondary-900) 8%, transparent)' : 'var(--color-secondary-800)',
'--button-bg-pressed': isSoft ? 'color-mix(in srgb, var(--color-secondary-900) 12%, transparent)' : 'var(--color-secondary-900)',
'--button-fg': isSoft ? 'var(--color-secondary-900)' : '#ffffff',
} as React.CSSProperties
}
function resolveSemanticColorClasses(color: NonNullable<ButtonProps['color']>, variant: ButtonProps['variant']): string {
const isSoft = variant === 'flat' || variant === 'light'
if (color === 'success') return isSoft ? 'bg-fifth/10 text-fifth' : 'bg-fifth text-white'
if (color === 'warning') return isSoft ? 'bg-fourth-900/10 text-fourth-900' : 'bg-fourth-900 text-tertiary'
if (color === 'secondary') return isSoft ? 'bg-tertiary/10 text-tertiary' : 'bg-tertiary text-tertiary'
return ''
}
// Using forwardRef
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-pressed': ariaPressed,
'aria-checked': ariaChecked,
'aria-selected': ariaSelected,
children,
color = 'primary',
size,
variant,
type = 'button',
target = '_self',
radius = 'sm',
role,
disabled,
isLoading,
iconOnly,
fullWidth,
iconStart,
iconEnd,
className,
to,
onClick,
},
ref
) => {
const isLink = Boolean(to)
const resolvedVariant = resolveVariant(color, variant)
const resolvedSize = size === 'xl' ? 'lg' : size
const semanticColorStyle = resolveSemanticColorStyle(color, variant)
const resolvedClassName =
`${size === 'xl' ? 'button--xl' : 'font-semibold'} shadow-none transition-all ${resolveSemanticColorClasses(color, variant)} ${
radius === 'none'
? 'rounded-none'
: radius === 'full'
? 'rounded-full'
: radius === 'sm'
? 'rounded-lg'
: radius === 'md'
? 'rounded-xl'
: 'rounded-2xl'
} ${className ?? ''}`.trim()
const ariaProps = {
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-pressed': ariaPressed,
'aria-checked': ariaChecked,
'aria-selected': ariaSelected,
role,
}
if (isLink && to) {
return (
<Link
{...ariaProps}
className={buttonVariants({
className: resolvedClassName,
fullWidth,
isIconOnly: iconOnly,
size: resolvedSize,
variant: resolvedVariant,
})}
href={to}
style={semanticColorStyle}
target={target}
onClick={onClick}
>
{iconStart}
{children}
{iconEnd}
</Link>
)
}
return (
<HeroButton
ref={ref}
{...ariaProps}
className={resolvedClassName}
fullWidth={fullWidth}
isDisabled={disabled}
isIconOnly={iconOnly}
isPending={isLoading}
size={resolvedSize}
style={semanticColorStyle}
type={type}
variant={resolvedVariant}
onPress={onClick}
>
{isLoading ? (
<Spinner
color="current"
size="sm"
/>
) : (
<>
{iconStart}
{children}
{iconEnd}
</>
)}
</HeroButton>
)
}
)
Button.displayName = 'Button'
export default Button