Files
SkinbaseNova/resources/js/components/ui/Button.jsx
Gregor Klevze a875203482 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
2026-03-01 10:41:43 +01:00

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>
)
}