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:
83
resources/js/components/ui/Button.jsx
Normal file
83
resources/js/components/ui/Button.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Nova Button
|
||||
*
|
||||
* @prop {string} variant - 'primary' | 'secondary' | 'ghost' | 'danger' | 'success' | 'accent'
|
||||
* @prop {string} size - 'xs' | 'sm' | 'md' | 'lg'
|
||||
* @prop {boolean} loading - shows spinner, disables button
|
||||
* @prop {boolean} iconOnly - makes button square (for icon-only buttons)
|
||||
* @prop {React.ReactNode} leftIcon / rightIcon - icon elements
|
||||
*/
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-sky-600 hover:bg-sky-500 text-white shadow-lg shadow-sky-600/20 focus-visible:ring-sky-500/50',
|
||||
accent: 'bg-accent hover:bg-accent/90 text-white shadow-lg shadow-accent/25 focus-visible:ring-accent/50',
|
||||
secondary: 'bg-white/8 hover:bg-white/14 text-white border border-white/15 focus-visible:ring-white/30',
|
||||
ghost: 'bg-transparent hover:bg-white/8 text-slate-300 hover:text-white focus-visible:ring-white/20',
|
||||
danger: 'bg-red-600 hover:bg-red-700 text-white shadow-lg shadow-red-600/20 focus-visible:ring-red-500/50',
|
||||
success: 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-600/20 focus-visible:ring-emerald-500/50',
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xs: 'px-2.5 py-1 text-xs rounded-lg gap-1.5',
|
||||
sm: 'px-3.5 py-1.5 text-sm rounded-xl gap-2',
|
||||
md: 'px-5 py-2.5 text-sm rounded-xl gap-2',
|
||||
lg: 'px-6 py-3 text-base rounded-xl gap-2.5',
|
||||
}
|
||||
|
||||
const iconOnlySizeClasses = {
|
||||
xs: 'w-7 h-7 rounded-lg',
|
||||
sm: 'w-8 h-8 rounded-lg',
|
||||
md: 'w-10 h-10 rounded-xl',
|
||||
lg: 'w-12 h-12 rounded-xl',
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
iconOnly = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
disabled,
|
||||
className = '',
|
||||
children,
|
||||
type = 'button',
|
||||
...rest
|
||||
}) {
|
||||
const isDisabled = disabled || loading
|
||||
|
||||
const base = [
|
||||
'inline-flex items-center justify-center font-semibold transition-all duration-150',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
||||
variantClasses[variant] ?? variantClasses.primary,
|
||||
iconOnly ? iconOnlySizeClasses[size] ?? iconOnlySizeClasses.md : sizeClasses[size] ?? sizeClasses.md,
|
||||
className,
|
||||
].join(' ')
|
||||
|
||||
return (
|
||||
<button type={type} disabled={isDisabled} className={base} {...rest}>
|
||||
{loading ? (
|
||||
<svg
|
||||
className="animate-spin shrink-0"
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||
</svg>
|
||||
) : leftIcon ? (
|
||||
<span className="shrink-0">{leftIcon}</span>
|
||||
) : null}
|
||||
|
||||
{!iconOnly && children && <span>{children}</span>}
|
||||
{iconOnly && !loading && children}
|
||||
|
||||
{rightIcon && !loading && <span className="shrink-0">{rightIcon}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user