- 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
88 lines
2.9 KiB
JavaScript
88 lines
2.9 KiB
JavaScript
import React, { forwardRef } from 'react'
|
||
|
||
/**
|
||
* Nova Select – styled native <select>
|
||
*
|
||
* Accepts the same options API as a plain <select>:
|
||
* - Pass children (<option>, <optgroup>) directly, OR
|
||
* - Pass `options` array of { value, label } and optional `placeholder`
|
||
*
|
||
* @prop {Array} options - [{ value, label }] – optional shorthand
|
||
* @prop {string} placeholder - adds a blank first option when using `options`
|
||
* @prop {string} label - field label
|
||
* @prop {string} error - validation error
|
||
* @prop {string} hint - helper text
|
||
* @prop {boolean} required - asterisk on label
|
||
* @prop {string} size - 'sm' | 'md' | 'lg'
|
||
*/
|
||
const Select = forwardRef(function Select(
|
||
{ label, error, hint, required, options, placeholder, size = 'md', id, className = '', children, ...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 inputClass = [
|
||
'block w-full rounded-xl border bg-white/[0.06] text-white',
|
||
'pl-3.5 pr-9',
|
||
'appearance-none cursor-pointer',
|
||
'bg-no-repeat bg-right',
|
||
'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,
|
||
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">
|
||
<select
|
||
id={inputId}
|
||
ref={ref}
|
||
className={inputClass}
|
||
aria-invalid={!!error}
|
||
{...rest}
|
||
>
|
||
{placeholder && <option value="" className="bg-nova-900">{placeholder}</option>}
|
||
|
||
{options
|
||
? options.map((o) => (
|
||
<option key={o.value} value={o.value} className="bg-nova-900 text-white">
|
||
{o.label}
|
||
</option>
|
||
))
|
||
: children}
|
||
</select>
|
||
|
||
{/* Custom chevron */}
|
||
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-slate-500">
|
||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
|
||
{error && <p role="alert" className="text-xs text-red-400">{error}</p>}
|
||
{!error && hint && <p className="text-xs text-slate-500">{hint}</p>}
|
||
</div>
|
||
)
|
||
})
|
||
|
||
export default Select
|