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:
113
resources/js/components/ui/Checkbox.jsx
Normal file
113
resources/js/components/ui/Checkbox.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
/**
|
||||
* Nova Checkbox – fully custom rendering (appearance-none + SVG tick).
|
||||
* Avoids @tailwindcss/forms overriding the checked background colour.
|
||||
*
|
||||
* @prop {string} label - label text rendered alongside the box
|
||||
* @prop {string} hint - small helper line below label
|
||||
* @prop {string} error - inline error
|
||||
* @prop {number|string} size - pixel size (default 18)
|
||||
* @prop {string} variant - 'accent' | 'emerald' | 'sky'
|
||||
*/
|
||||
const variantStyles = {
|
||||
accent: { checked: '#E07A21', ring: 'rgba(224,122,33,0.45)' },
|
||||
emerald: { checked: '#10b981', ring: 'rgba(16,185,129,0.45)' },
|
||||
sky: { checked: '#0ea5e9', ring: 'rgba(14,165,233,0.45)' },
|
||||
}
|
||||
|
||||
const Checkbox = forwardRef(function Checkbox(
|
||||
{ label, hint, error, size = 18, variant = 'accent', id, className = '', checked, disabled, onChange, ...rest },
|
||||
ref,
|
||||
) {
|
||||
const dim = typeof size === 'number' ? `${size}px` : size
|
||||
const numSize = typeof size === 'number' ? size : parseInt(size, 10)
|
||||
const inputId = id ?? (label ? `cb-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined)
|
||||
const colors = variantStyles[variant] ?? variantStyles.accent
|
||||
|
||||
// Tick sizes relative to box
|
||||
const tickInset = Math.round(numSize * 0.18)
|
||||
const strokeWidth = Math.max(1.5, numSize * 0.1)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
className={[
|
||||
'inline-flex items-start gap-2.5 select-none',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
||||
].join(' ')}
|
||||
>
|
||||
{/* Hidden native input keeps full a11y / form submission */}
|
||||
<input
|
||||
type="checkbox"
|
||||
id={inputId}
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className="sr-only"
|
||||
aria-invalid={!!error}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{/* Visual box */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: dim,
|
||||
height: dim,
|
||||
minWidth: dim,
|
||||
minHeight: dim,
|
||||
aspectRatio: '1 / 1',
|
||||
marginTop: label ? '1px' : undefined,
|
||||
backgroundColor: checked ? colors.checked : 'rgba(255,255,255,0.06)',
|
||||
borderColor: checked ? colors.checked : 'rgba(255,255,255,0.25)',
|
||||
boxShadow: checked ? `0 0 0 0px ${colors.ring}` : undefined,
|
||||
transition: 'background-color 150ms, border-color 150ms',
|
||||
}}
|
||||
className={[
|
||||
'shrink-0 inline-flex items-center justify-center',
|
||||
'rounded-md border',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
{/* SVG tick — only visible when checked */}
|
||||
<svg
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
style={{
|
||||
width: numSize - tickInset * 2,
|
||||
height: numSize - tickInset * 2,
|
||||
opacity: checked ? 1 : 0,
|
||||
transition: 'opacity 100ms',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M1.5 6l3 3 6-6"
|
||||
stroke="white"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{(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>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-xs text-red-400" style={{ paddingLeft: `calc(${dim} + 0.625rem)` }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Checkbox
|
||||
Reference in New Issue
Block a user