Replace native selects with NovaSelect

This commit is contained in:
2026-05-01 07:45:37 +02:00
parent 67be537c86
commit 35011001ba
55 changed files with 3136 additions and 1662 deletions

View File

@@ -1,9 +1,10 @@
import React, { forwardRef } from 'react'
import React, { Children } from 'react'
import NovaSelect from './NovaSelect'
/**
* Nova Select styled native <select>
* Legacy Select wrapper.
*
* Accepts the same options API as a plain <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`
*
@@ -13,82 +14,46 @@ import React, { forwardRef } from 'react'
* @prop {string} error - validation error
* @prop {string} hint - helper text
* @prop {boolean} required - asterisk on label
* @prop {string} size - 'sm' | 'md' | 'lg'
* @prop {string} size - ignored, kept for backward compatibility
*/
const Select = forwardRef(function Select(
{ label, error, hint, required, options, placeholder, size = 'md', id, className = '', children, style, ...rest },
ref,
) {
const inputId = id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined)
function Select({ label, error, hint, required, options, placeholder, size = 'md', id, className = '', children, ...rest }) {
void size
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 normalizedOptions = options || Children.toArray(children).flatMap((child) => {
if (!React.isValidElement(child)) return []
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(' ')
if (child.type === 'optgroup') {
const groupLabel = child.props.label
return Children.toArray(child.props.children)
.filter((optionChild) => React.isValidElement(optionChild))
.map((optionChild) => ({
value: optionChild.props.value,
label: optionChild.props.children,
group: groupLabel,
disabled: optionChild.props.disabled,
}))
}
return [{
value: child.props.value,
label: child.props.children,
disabled: child.props.disabled,
}]
})
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}
style={{
appearance: 'none',
WebkitAppearance: 'none',
MozAppearance: 'none',
backgroundImage: 'none',
...style,
}}
{...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>
<NovaSelect
id={id}
label={label}
error={error}
hint={hint}
required={required}
options={normalizedOptions}
placeholder={placeholder}
className={className}
{...rest}
/>
)
})
}
export default Select