- Add Nova UI library: Button, TextInput, Textarea, FormField, Select, NovaSelect, Checkbox, Radio/RadioGroup, Toggle, DatePicker, DateRangePicker, Modal + barrel index.js - Replace all native <select> in Studio with NovaSelect (StudioFilters, StudioToolbar, BulkActionsBar) including frosted-glass portal and category group headers - Replace native checkboxes in StudioGridCard, StudioTable, UploadSidebar, UploadWizard, Upload/Index with custom Checkbox component - Add nova-scrollbar CSS utility (thin 4px, semi-transparent) - Fix portal position drift: use viewport-relative coords (no scrollY offset) for NovaSelect, DatePicker and DateRangePicker - Close portals on external scroll instead of remeasuring - Improve hover highlight visibility in NovaSelect (bg-white/[0.13]) - Move search icon to right side in NovaSelect dropdown - Reduce Studio layout top spacing (py-6 -> pt-4 pb-8) - Add StudioCheckbox and SquareCheckbox backward-compat shims - Add sync.sh rsync deploy script
103 lines
2.8 KiB
JavaScript
103 lines
2.8 KiB
JavaScript
import React, { forwardRef } from 'react'
|
|
|
|
/**
|
|
* Nova TextInput
|
|
*
|
|
* @prop {string} label - optional label above field
|
|
* @prop {string} error - validation error message
|
|
* @prop {string} hint - helper text below field
|
|
* @prop {React.ReactNode} leftIcon - icon/element to show inside left side
|
|
* @prop {React.ReactNode} rightIcon - icon/element to show inside right side
|
|
* @prop {boolean} required - shows red asterisk on label
|
|
* @prop {string} size - 'sm' | 'md' | 'lg'
|
|
*/
|
|
const TextInput = forwardRef(function TextInput(
|
|
{
|
|
label,
|
|
error,
|
|
hint,
|
|
leftIcon,
|
|
rightIcon,
|
|
required,
|
|
size = 'md',
|
|
id,
|
|
className = '',
|
|
...rest
|
|
},
|
|
ref,
|
|
) {
|
|
const inputId = id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined)
|
|
|
|
const sizeClass = {
|
|
sm: 'py-1.5 text-xs',
|
|
md: 'py-2.5 text-sm',
|
|
lg: 'py-3 text-base',
|
|
}[size] ?? 'py-2.5 text-sm'
|
|
|
|
const paddingLeft = leftIcon ? 'pl-10' : 'pl-3.5'
|
|
const paddingRight = rightIcon ? 'pr-10' : 'pr-3.5'
|
|
|
|
const inputClass = [
|
|
'block w-full rounded-xl border bg-white/[0.06] text-white',
|
|
'placeholder:text-slate-500',
|
|
'transition-all duration-150',
|
|
'focus:outline-none focus:ring-2 focus:ring-offset-0',
|
|
error
|
|
? 'border-red-500/60 focus:border-red-500/70 focus:ring-red-500/40'
|
|
: 'border-white/12 hover:border-white/20 focus:border-accent/50 focus:ring-accent/40',
|
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
sizeClass,
|
|
paddingLeft,
|
|
paddingRight,
|
|
className,
|
|
].join(' ')
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1.5">
|
|
{label && (
|
|
<label htmlFor={inputId} className="text-sm font-medium text-white/85">
|
|
{label}
|
|
{required && <span className="text-red-400 ml-1">*</span>}
|
|
</label>
|
|
)}
|
|
|
|
<div className="relative">
|
|
{leftIcon && (
|
|
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500 pointer-events-none">
|
|
{leftIcon}
|
|
</span>
|
|
)}
|
|
|
|
<input
|
|
id={inputId}
|
|
ref={ref}
|
|
className={inputClass}
|
|
aria-invalid={!!error}
|
|
aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
|
|
{...rest}
|
|
/>
|
|
|
|
{rightIcon && (
|
|
<span className="absolute right-3.5 top-1/2 -translate-y-1/2 text-slate-500">
|
|
{rightIcon}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<p id={`${inputId}-error`} role="alert" className="text-xs text-red-400">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{!error && hint && (
|
|
<p id={`${inputId}-hint`} className="text-xs text-slate-500">
|
|
{hint}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
})
|
|
|
|
export default TextInput
|