admin/components/heroui/Input.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

242 lines
7.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import type { ChangeEvent, ComponentProps, ReactNode } from 'react'
import { forwardRef } from 'react'
import { Description, FieldError, type Input as HeroInput, InputGroup, Label, TextArea as HeroTextarea, TextField } from '@heroui/react'
import { cn } from '@/lib/cn'
interface LegacyClassNames {
base?: string
input?: string
inputWrapper?: string
innerWrapper?: string
label?: string
}
interface SharedProps {
'aria-label'?: string
autoFocus?: boolean
className?: string
classNames?: LegacyClassNames
defaultValue?: string
description?: ReactNode
descriptionClassName?: string
disabled?: boolean
endContent?: ReactNode
errorMessage?: ReactNode
isClearable?: boolean
isDisabled?: boolean
isInvalid?: boolean
isReadOnly?: boolean
isRequired?: boolean
label?: ReactNode
labelPlacement?: 'inside' | 'outside' | 'outside-left'
name?: string
placeholder?: string
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
size?: 'sm' | 'md' | 'lg'
startContent?: ReactNode
validationBehavior?: 'native' | 'aria'
value?: string
variant?: 'flat' | 'bordered' | 'faded' | 'underlined'
onBlur?: ComponentProps<typeof HeroInput>['onBlur']
onChange?: (event: ChangeEvent<HTMLInputElement>) => void
onClear?: () => void
onValueChange?: (value: string) => void
}
export type InputProps = SharedProps &
Omit<
ComponentProps<typeof HeroInput>,
'children' | 'className' | 'defaultValue' | 'disabled' | 'onChange' | 'readOnly' | 'required' | 'size' | 'value' | 'variant'
>
function radiusClass(radius: SharedProps['radius']): string {
if (!radius) return ''
if (radius === 'none') return 'rounded-none'
if (radius === 'full') return 'rounded-full'
if (radius === 'lg') return 'rounded-2xl'
if (radius === 'sm') return 'rounded-md'
return 'rounded-lg'
}
function sizeClass(size: SharedProps['size']): string {
if (size === 'sm') return 'min-h-8'
if (size === 'lg') return 'min-h-12'
return 'min-h-10'
}
function variantClass(variant: SharedProps['variant'], invalid?: boolean): string {
if (invalid) return 'border-fourth'
if (variant === 'underlined') return 'rounded-none border-x-0 border-t-0'
if (variant === 'flat' || variant === 'faded') return 'border-transparent bg-default-100'
return 'border-divider bg-surface'
}
const clearButtonClassName =
'flex size-5 shrink-0 items-center justify-center rounded-full bg-default-200 text-sm leading-none text-default-500 transition-colors hover:bg-default-300'
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{
className,
classNames,
description,
descriptionClassName,
disabled,
endContent,
errorMessage,
isClearable,
isDisabled,
isInvalid,
isReadOnly,
isRequired,
label,
labelPlacement: _labelPlacement,
radius,
size,
startContent,
validationBehavior,
variant,
onChange,
onClear,
onValueChange,
...props
},
ref
) {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange?.(event)
onValueChange?.(event.target.value)
}
return (
<TextField
className={cn('flex min-w-0 w-full flex-col gap-1', classNames?.base, className)}
isDisabled={isDisabled || disabled}
isInvalid={isInvalid}
isReadOnly={isReadOnly}
isRequired={isRequired}
validationBehavior={validationBehavior}
>
{label ? <Label className={cn('labelClass', classNames?.label)}>{label}</Label> : null}
<InputGroup
fullWidth
className={cn(
'relative',
!classNames?.inputWrapper && cn('border', radiusClass(radius), sizeClass(size), variantClass(variant, isInvalid)),
classNames?.inputWrapper,
classNames?.innerWrapper
)}
variant={variant === 'bordered' || variant === 'underlined' ? 'secondary' : 'primary'}
>
{startContent ? <InputGroup.Prefix>{startContent}</InputGroup.Prefix> : null}
<InputGroup.Input
{...props}
ref={ref}
className={cn(isClearable && props.value && 'pl-8', classNames?.input)}
onChange={handleChange}
/>
{isClearable && props.value ? (
<button
aria-label="پاک کردن"
className={cn(clearButtonClassName, 'absolute left-2 top-1/2 z-10 -translate-y-1/2')}
type="button"
onClick={onClear}
>
×
</button>
) : endContent ? (
<InputGroup.Suffix>{endContent}</InputGroup.Suffix>
) : null}
</InputGroup>
{description ? <Description className={descriptionClassName}>{description}</Description> : null}
{isInvalid && errorMessage ? <FieldError>{errorMessage}</FieldError> : null}
</TextField>
)
})
export type TextareaProps = Omit<SharedProps, 'onBlur' | 'onChange'> &
Omit<
ComponentProps<typeof HeroTextarea>,
'children' | 'className' | 'defaultValue' | 'disabled' | 'onChange' | 'readOnly' | 'required' | 'size' | 'value' | 'variant'
> & {
minRows?: number
onBlur?: ComponentProps<typeof HeroTextarea>['onBlur']
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void
}
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
{
className,
classNames,
description,
descriptionClassName,
disabled,
errorMessage,
isClearable,
isDisabled,
isInvalid,
isReadOnly,
isRequired,
label,
labelPlacement: _labelPlacement,
minRows,
radius,
size,
validationBehavior,
variant,
onClear,
onValueChange,
...props
},
ref
) {
return (
<TextField
className={cn('flex min-w-0 w-full flex-col gap-1', classNames?.base, className)}
isDisabled={isDisabled || disabled}
isInvalid={isInvalid}
isReadOnly={isReadOnly}
isRequired={isRequired}
validationBehavior={validationBehavior}
>
{label ? <Label className={cn('labelClass', classNames?.label)}>{label}</Label> : null}
<div className="relative">
<HeroTextarea
{...props}
ref={ref}
fullWidth
className={cn(
!classNames?.inputWrapper && cn('border p-3', radiusClass(radius), sizeClass(size), variantClass(variant, isInvalid)),
classNames?.inputWrapper,
classNames?.input
)}
rows={minRows}
variant={variant === 'bordered' || variant === 'underlined' ? 'secondary' : 'primary'}
onChange={(event) => {
props.onChange?.(event)
onValueChange?.(event.target.value)
}}
/>
{isClearable && props.value ? (
<button
aria-label="پاک کردن"
className={cn(clearButtonClassName, 'absolute left-2 top-2')}
type="button"
onClick={onClear}
>
×
</button>
) : null}
</div>
{description ? <Description className={descriptionClassName}>{description}</Description> : null}
{isInvalid && errorMessage ? <FieldError>{errorMessage}</FieldError> : null}
</TextField>
)
})