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

108 lines
3.4 KiB
TypeScript
Raw 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 { ComponentProps, ReactNode } from 'react'
import { Description, FieldError, Label, NumberField } from '@heroui/react'
import { cn } from '@/lib/cn'
type NumberInputProps = Omit<ComponentProps<typeof NumberField>, 'children' | 'className' | 'onChange' | 'variant'> & {
className?: string
classNames?: { inputWrapper?: string; label?: string; input?: string }
description?: ReactNode
descriptionClassName?: string
endContent?: ReactNode
errorMessage?: ReactNode
formatOptions?: Intl.NumberFormatOptions
isClearable?: boolean
label?: ReactNode
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
size?: 'sm' | 'md' | 'lg'
startContent?: ReactNode
variant?: 'flat' | 'bordered' | 'faded' | 'underlined'
onClear?: () => void
onValueChange?: (value: number) => void
}
function radiusClass(radius: NumberInputProps['radius'], variant: NumberInputProps['variant']): string {
if (variant === 'underlined') return 'rounded-none'
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'
}
export function NumberInput({
className,
classNames,
description,
descriptionClassName,
endContent,
errorMessage,
isClearable,
isInvalid,
label,
radius,
size,
startContent,
variant,
value,
onClear,
onValueChange,
...props
}: NumberInputProps) {
return (
<NumberField
{...props}
className={cn('flex w-full flex-col gap-1', className)}
isInvalid={isInvalid}
value={value}
variant={variant === 'bordered' || variant === 'underlined' ? 'secondary' : 'primary'}
onChange={onValueChange}
>
{label ? <Label className={cn('labelClass', classNames?.label)}>{label}</Label> : null}
<NumberField.Group
className={cn(
'relative flex w-full items-center',
!classNames?.inputWrapper &&
cn(
'border border-divider bg-surface px-3',
size === 'sm' ? 'min-h-8' : size === 'lg' ? 'min-h-12' : 'min-h-10',
radiusClass(radius, variant)
),
classNames?.inputWrapper,
// Always keep a single red border on error (never HeroUI invalid-field-ring outline).
isInvalid && '!border-fourth hover:!border-fourth focus-within:!border-fourth data-[invalid=true]:!border-fourth'
)}
>
{startContent}
<NumberField.Input
className={cn(
'min-w-0 flex-1 bg-transparent text-left outline-none',
classNames?.input,
isClearable && value !== undefined && value !== null && 'pl-8'
)}
dir="ltr"
/>
{isClearable && value !== undefined && value !== null ? (
<button
aria-label="پاک کردن"
className="absolute left-2 top-1/2 z-10 flex size-5 -translate-y-1/2 shrink-0 items-center justify-center rounded-full bg-default-200 text-sm leading-none text-default-500 transition-colors hover:bg-default-300"
type="button"
onClick={onClear}
>
×
</button>
) : (
endContent
)}
</NumberField.Group>
{description ? <Description className={descriptionClassName}>{description}</Description> : null}
{isInvalid && errorMessage ? <FieldError>{errorMessage}</FieldError> : null}
</NumberField>
)
}