- 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
84 lines
3.0 KiB
JavaScript
84 lines
3.0 KiB
JavaScript
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>
|
|
)
|
|
}
|