89 lines
2.3 KiB
TypeScript
89 lines
2.3 KiB
TypeScript
import { useId } from 'react'
|
|
import type { InputHTMLAttributes, ReactNode } from 'react'
|
|
import type { LucideIcon } from 'lucide-react'
|
|
import { cn } from '../../lib/cn.ts'
|
|
|
|
export const FIELD_BASE =
|
|
'w-full rounded-lg border bg-surface-2 text-fg placeholder:text-fg-subtle transition-colors focus:outline-none focus-visible:border-accent disabled:opacity-60'
|
|
|
|
export function FieldFrame({
|
|
id,
|
|
label,
|
|
hint,
|
|
error,
|
|
children,
|
|
}: {
|
|
id: string
|
|
label?: ReactNode
|
|
hint?: ReactNode
|
|
error?: ReactNode
|
|
children: ReactNode
|
|
}) {
|
|
return (
|
|
<div className="flex min-w-0 flex-col gap-1.5">
|
|
{label !== undefined && (
|
|
<label htmlFor={id} className="text-xs font-medium text-fg-muted">
|
|
{label}
|
|
</label>
|
|
)}
|
|
{children}
|
|
{error !== undefined && error !== null && error !== '' ? (
|
|
<p id={`${id}-error`} role="alert" className="text-xs text-danger">
|
|
{error}
|
|
</p>
|
|
) : hint !== undefined ? (
|
|
<p id={`${id}-hint`} className="text-xs text-fg-subtle">
|
|
{hint}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
label?: ReactNode
|
|
hint?: ReactNode
|
|
error?: ReactNode
|
|
icon?: LucideIcon
|
|
className?: string
|
|
}
|
|
|
|
export function Input({ label, hint, error, icon: Icon, className, id, ...props }: InputProps) {
|
|
const autoId = useId()
|
|
const inputId = id ?? autoId
|
|
const describedBy = error ? `${inputId}-error` : hint !== undefined ? `${inputId}-hint` : undefined
|
|
|
|
const input = (
|
|
<div className="relative">
|
|
{Icon && (
|
|
<Icon
|
|
size={15}
|
|
strokeWidth={2}
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-fg-subtle"
|
|
/>
|
|
)}
|
|
<input
|
|
id={inputId}
|
|
aria-invalid={error ? true : undefined}
|
|
aria-describedby={describedBy}
|
|
className={cn(
|
|
FIELD_BASE,
|
|
'h-9 px-2.5 text-sm',
|
|
Icon && 'pl-8',
|
|
error ? 'border-danger' : 'border-line',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
)
|
|
|
|
if (label === undefined && hint === undefined && error === undefined) return input
|
|
return (
|
|
<FieldFrame id={inputId} label={label} hint={hint} error={error}>
|
|
{input}
|
|
</FieldFrame>
|
|
)
|
|
}
|