Files
SkinbaseNova/resources/js/components/ui/Textarea.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

66 lines
2.0 KiB
JavaScript

import React, { forwardRef } from 'react'
/**
* Nova Textarea
*
* @prop {string} label - optional label
* @prop {string} error - validation error
* @prop {string} hint - helper text
* @prop {boolean} required - red asterisk on label
* @prop {number} rows - visible rows (default 4)
* @prop {boolean} resize - allow manual resize (default false)
*/
const Textarea = forwardRef(function Textarea(
{ label, error, hint, required, rows = 4, resize = false, id, className = '', ...rest },
ref,
) {
const inputId = id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined)
const inputClass = [
'block w-full rounded-xl border bg-white/[0.06] text-white text-sm',
'px-3.5 py-2.5 placeholder:text-slate-500',
'transition-all duration-150',
'focus:outline-none focus:ring-2 focus:ring-offset-0',
resize ? 'resize-y' : 'resize-none',
error
? 'border-red-500/60 focus:border-red-500/70 focus:ring-red-500/40'
: 'border-white/12 hover:border-white/20 focus:border-accent/50 focus:ring-accent/40',
'disabled:opacity-50 disabled:cursor-not-allowed',
className,
].join(' ')
return (
<div className="flex flex-col gap-1.5">
{label && (
<label htmlFor={inputId} className="text-sm font-medium text-white/85">
{label}
{required && <span className="text-red-400 ml-1">*</span>}
</label>
)}
<textarea
id={inputId}
ref={ref}
rows={rows}
className={inputClass}
aria-invalid={!!error}
aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
{...rest}
/>
{error && (
<p id={`${inputId}-error`} role="alert" className="text-xs text-red-400">
{error}
</p>
)}
{!error && hint && (
<p id={`${inputId}-hint`} className="text-xs text-slate-500">
{hint}
</p>
)}
</div>
)
})
export default Textarea