Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
185 lines
5.6 KiB
TypeScript
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
|