feat: Nova UI component library + Studio dropdown/picker polish
- 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
This commit is contained in:
93
resources/js/components/ui/Radio.jsx
Normal file
93
resources/js/components/ui/Radio.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
/**
|
||||
* Nova Radio – single choice radio button.
|
||||
*
|
||||
* Usage (as a group):
|
||||
* {options.map(o => (
|
||||
* <Radio key={o.value} value={o.value} checked={val===o.value} onChange={setVal} label={o.label} />
|
||||
* ))}
|
||||
*
|
||||
* Or use RadioGroup for a pre-built grouped set.
|
||||
*/
|
||||
export const Radio = forwardRef(function Radio(
|
||||
{ label, hint, size = 18, id, className = '', ...rest },
|
||||
ref,
|
||||
) {
|
||||
const dim = typeof size === 'number' ? `${size}px` : size
|
||||
const inputId = id ?? (label ? `radio-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined)
|
||||
|
||||
return (
|
||||
<label className="inline-flex items-start gap-2.5 cursor-pointer select-none">
|
||||
<input
|
||||
type="radio"
|
||||
id={inputId}
|
||||
ref={ref}
|
||||
className={[
|
||||
'shrink-0 cursor-pointer',
|
||||
'border border-white/25 bg-white/8',
|
||||
'text-accent checked:bg-accent checked:border-accent',
|
||||
'transition-colors duration-150',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 focus-visible:ring-offset-0',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
className,
|
||||
].join(' ')}
|
||||
style={{
|
||||
width: dim,
|
||||
height: dim,
|
||||
minWidth: dim,
|
||||
minHeight: dim,
|
||||
aspectRatio: '1 / 1',
|
||||
marginTop: label ? '1px' : undefined,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{(label || hint) && (
|
||||
<span className="flex flex-col gap-0.5">
|
||||
{label && <span className="text-sm text-white/90 leading-snug">{label}</span>}
|
||||
{hint && <span className="text-xs text-slate-500">{hint}</span>}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* RadioGroup – renders a set of Radio buttons from an options array.
|
||||
*
|
||||
* @prop {Array} options - [{ value, label, hint? }]
|
||||
* @prop {string} value - currently selected value
|
||||
* @prop {function} onChange - called with new value string
|
||||
* @prop {string} name - unique name for radio group (required)
|
||||
* @prop {string} label - group label
|
||||
* @prop {string} error - validation error
|
||||
* @prop {'vertical'|'horizontal'} direction
|
||||
*/
|
||||
export function RadioGroup({ options = [], value, onChange, name, label, error, direction = 'vertical', className = '' }) {
|
||||
return (
|
||||
<fieldset className={`flex flex-col gap-1.5 ${className}`}>
|
||||
{label && (
|
||||
<legend className="text-sm font-medium text-white/85 mb-1">{label}</legend>
|
||||
)}
|
||||
|
||||
<div className={`flex gap-3 ${direction === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col'}`}>
|
||||
{options.map((opt) => (
|
||||
<Radio
|
||||
key={opt.value}
|
||||
name={name}
|
||||
value={opt.value}
|
||||
checked={value === opt.value}
|
||||
onChange={() => onChange(opt.value)}
|
||||
label={opt.label}
|
||||
hint={opt.hint}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <p role="alert" className="text-xs text-red-400">{error}</p>}
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
export default Radio
|
||||
Reference in New Issue
Block a user