Ir al contenido

Formularios

Veinte símbolos: cuatro para montar la estructura del formulario y quince controles de entrada. Todos los controles siguen el tema del tenant y comparten el mismo vocabulario de props —label, error, helperText, required, disabled—, así que lo que aprendes en uno vale para el resto.

Envuelve cualquier control y le pone etiqueta, ayuda y error, enlazando la etiqueta con el control por htmlFor. Funciona también con controles que no son de la librería.

import { Field, Input, Textarea } from '@openfactu/ui';
<Field label="Razón social" required>
<Input defaultValue="Acme S.L." />
</Field>
<Field label="Notas" hint="Solo visible para tu equipo.">
<Textarea rows={3} />
</Field>
<Field label="CIF" error="El formato no es válido.">
<Input defaultValue="12345678" />
</Field>
<Field label="Control ajeno a la librería" hint="Un <input> corriente.">
<input className="" />
</Field>
<Field label="md · por defecto" labelSize="md"><Input /></Field>
<Field label="sm · formularios densos" labelSize="sm"><Input inputSize="sm" /></Field>
<Field label="micro · rejillas de datos" labelSize="micro"><Input inputSize="sm" /></Field>
<Field label="Cliente activo" orientation="horizontal" hint="Si se desactiva no se le podrán emitir documentos.">
<Switch checked={activo} onChange={setActivo} />
</Field>
Prop Tipo Por defecto Descripción
children ReactNode El control
label ReactNode Etiqueta
hint ReactNode Texto de ayuda bajo el control. Lo tapa error
error string Mensaje de error
required boolean Marca de campo obligatorio
labelSize 'sm' | 'md' | 'micro' 'md' micro son versales diminutas, para rejillas densas
orientation 'vertical' | 'horizontal' 'vertical' horizontal pone la etiqueta a la izquierda
labelWidth string '9rem' Ancho de la etiqueta en horizontal
htmlFor string generado id del control, para enlazar la etiqueta
className string Clases del contenedor
labelClassName string Clases de la etiqueta

FormSection agrupa campos bajo un título; FormGrid los reparte en columnas que colapsan a una en pantalla estrecha; FormRow hace que un campo ocupe la fila entera.

import { FormSection, FormGrid, FormRow, Field, Input } from '@openfactu/ui';
<FormSection title="Identificación" description="Datos que aparecen en las facturas.">
<FormGrid>
<Field label="Razón social" required>
<Input defaultValue="Acme S.L." />
</Field>
<Field label="CIF" required>
<Input defaultValue="B12345678" />
</Field>
<FormRow>
<Field label="Dirección">
<Input defaultValue="Calle Mayor 1, 28013 Madrid" />
</Field>
</FormRow>
</FormGrid>
</FormSection>
<FormSection title="Condiciones comerciales" divider>
<FormGrid columns={3}>
<Field label="Forma de pago">
<Select value={pago} onChange={setPago} options={FORMAS_DE_PAGO} />
</Field>
<Field label="Límite de crédito">
<CurrencyInput value={limite} onChange={setLimite} />
</Field>
<Field label="Alta">
<DatePicker value={alta} onChange={setAlta} />
</Field>
</FormGrid>
</FormSection>

En Recetas tienes la ficha de cliente completa, con Card y botonera.

Prop Tipo Por defecto Descripción
children ReactNode Contenido de la sección
title ReactNode Título
description ReactNode Descripción bajo el título
actions ReactNode Acciones a la derecha del título
divider boolean true salvo en la primera Línea separadora encima
className string Clases del contenedor
Prop Tipo Por defecto Descripción
children ReactNode Campos
columns 1 | 2 | 3 | 4 2 Columnas en pantalla ancha
gap 'sm' | 'md' | 'lg' 'md' Separación entre campos
className string Clases del contenedor
Prop Tipo Por defecto Descripción
children ReactNode Contenido
full boolean Ocupa todas las columnas de la rejilla
className string Clases del contenedor

El campo de texto. Extiende los atributos nativos del <input> salvo size y prefix, cuyos nombres se reaprovechan con un significado más útil.

import { Input } from '@openfactu/ui';
<Input label="Nombre" placeholder="Acme S.L." />
<Input label="Obligatorio" required placeholder="No puede quedar vacío" />
<Input label="Con ayuda" helperText="Como aparece en el registro mercantil." />
<Input label="Con error" error="Este campo es obligatorio." />
<Input label="Deshabilitado" disabled defaultValue="No editable" />

Son dos cosas distintas: el icono flota dentro del campo, el complemento va pegado fuera, con su propio borde y fondo.

import { Mail, Search } from 'lucide-react';
{/* iconos */}
<Input label="Correo" leftIcon={<Mail className="h-3.5 w-3.5" />} placeholder="hola@acme.es" />
<Input label="Buscar" rightIcon={<Search className="h-3.5 w-3.5" />} placeholder="Filtrar…" />
{/* complementos */}
<Input label="Teléfono" prefix="+34" placeholder="600 000 000" />
<Input label="Sitio web" prefix="https://" placeholder="acme.es" />
<Input label="Descuento" suffix="%" defaultValue="21" className="text-right font-mono" />

Si el complemento lleva controles dentro (un botón, un select), marca prefixInteractive o suffixInteractive para que pierda el fondo y el relleno.

El icono de estado se pinta solo a partir de status.

const [cif, setCif] = React.useState('B12345678');
const valido = /^[A-Z]\d{8}$/.test(cif);
<Input
label="CIF"
value={cif}
onChange={(e) => setCif(e.target.value.toUpperCase())}
status={cif === '' ? 'default' : valido ? 'success' : 'error'}
statusMessage={cif === '' ? undefined : valido ? 'Formato correcto' : 'Formato no válido'}
/>
<Input label="Comprobando…" defaultValue="ES91 2100 0418 45" status="loading" />
<Input label="Aviso" defaultValue="Cuenta sin verificar" status="warning" statusMessage="Verifícala antes de emitir." />

error implica status="error", así que para el caso corriente basta con error.

Prop Tipo Por defecto Descripción
label ReactNode Etiqueta
error string Mensaje de error. Implica status="error"
helperText ReactNode Texto de ayuda
leftIcon ReactNode Icono superpuesto dentro del campo, a la izquierda
rightIcon ReactNode Icono superpuesto dentro del campo, a la derecha
prefix ReactNode Complemento pegado con su propio borde (+34, https://)
suffix ReactNode Complemento pegado a la derecha (%, , un botón)
prefixInteractive boolean false El complemento contiene controles: sin fondo ni relleno
suffixInteractive boolean false Ídem, a la derecha
status 'default' | 'success' | 'warning' | 'error' | 'loading' Estado de validación
statusMessage ReactNode Mensaje del estado, si no hay error
showStatusIcon boolean true si hay estado Pinta el icono del estado como sufijo
inputSize 'sm' | 'md' | 'lg' 'md' Tamaño
inputRef Ref<HTMLInputElement> Referencia al <input> interno
requiredMark boolean Asterisco de obligatorio sin poner el atributo nativo
containerClassName string Clases del contenedor
labelClassName string Clases de la etiqueta
…resto InputHTMLAttributes sin size ni prefix value, onChange, placeholder, required
import { Textarea } from '@openfactu/ui';
<Textarea label="Observaciones" rows={4} placeholder="Notas internas…" />
<Textarea label="Condiciones" helperText="Se imprimen al pie de la factura." />
<Textarea label="Motivo" error="Indica el motivo de la rectificación." />
Prop Tipo Descripción
label string Etiqueta
error string Mensaje de error
helperText string Texto de ayuda
containerClassName string Clases del contenedor
…resto TextareaHTMLAttributes rows, value, onChange, required

Desplegable de opción única, para listas cortas y cerradas. Si la lista es larga o hay que buscar, usa SearchableSelect.

import { Select } from '@openfactu/ui';
<Select
label="País"
required
value={pais}
onChange={setPais}
options={[
{ value: 'es', label: 'España' },
{ value: 'pt', label: 'Portugal' },
{ value: 'fr', label: 'Francia' },
]}
/>
<Select
label="Serie"
placeholder="Elige una serie…"
value={serie}
onChange={setSerie}
options={SERIES}
helperText="Determina la numeración del documento."
/>
Prop Tipo Descripción
options SelectOption[] Opciones: { value, label, disabled? }
value string Valor seleccionado
onChange (value: string) => void Cambio de selección
placeholder string Texto con la selección vacía
label string Etiqueta
required boolean Marca visible y aria-required
error string Mensaje de error
helperText string Texto de ayuda
disabled boolean Desactiva el control
ariaLabel string Nombre accesible cuando no hay label visible
id string id del control
className / containerClassName string Clases

El desplegable con buscador: filtra al teclear, resalta lo que coincide, agrupa, indenta jerarquías, permite selección múltiple, crear opciones nuevas y cargar desde servidor con paginación. Es el control más completo de la librería.

import { SearchableSelect } from '@openfactu/ui';
<SearchableSelect
label="Almacén"
required
value={almacen}
onChange={setAlmacen}
options={[
{ value: 'a', label: 'Almacén central' },
{ value: 'b', label: 'Tienda Norte' },
]}
helperText="Se usa para las salidas de stock."
/>

Cada valor elegido se pinta como un chip con su X. value y onChange pasan a trabajar con arrays.

const [etiquetas, setEtiquetas] = React.useState<string[]>(['2', '7']);
<SearchableSelect multiple clearable options={OPCIONES} value={etiquetas} onChange={setEtiquetas} />

group mete las opciones bajo encabezados; depth las indenta, que es lo que se quiere para categorías, plan contable o ubicaciones de almacén.

{/* por grupos */}
<SearchableSelect
value={cuenta}
onChange={setCuenta}
placeholder="Elige una cuenta…"
options={[
{ value: '430', label: '430 · Clientes', group: 'Activo' },
{ value: '572', label: '572 · Bancos', group: 'Activo' },
{ value: '400', label: '400 · Proveedores', group: 'Pasivo' },
{ value: '700', label: '700 · Venta de mercaderías', group: 'Ingresos' },
]}
/>
{/* jerárquico */}
<SearchableSelect
value={categoria}
onChange={setCategoria}
options={[
{ value: 'oficina', label: 'Material de oficina', depth: 1 },
{ value: 'papel', label: 'Papelería', depth: 2 },
{ value: 'info', label: 'Informática', depth: 1 },
{ value: 'portatiles', label: 'Portátiles', depth: 2 },
]}
/>

Con onSearchChange el filtrado en cliente se desactiva: le pasas tú las opciones ya filtradas. loading pinta el spinner dentro del listado.

<SearchableSelect
options={resultados}
value={cliente}
onChange={setCliente}
loading={cargando}
onSearchChange={buscarEnServidor}
emptyMessage="Ningún cliente coincide"
/>

Para paginación, hasMore más onLoadMore: se llama al llegar al final de la lista.

<SearchableSelect
creatable
createLabel="Crear «{term}»"
options={opciones}
value={valor}
onChange={setValor}
onCreate={async (termino) => {
const nueva = await crearEtiqueta(termino);
setOpciones((prev) => [...prev, nueva]);
return nueva.value;
}}
/>

onCreate recibe lo tecleado y devuelve el valor a seleccionar (o nada, para no seleccionar).

SearchableSelectProps es una unión: con multiple a false u omitido, value es un string; con multiple a true, un string[].

Prop Tipo Por defecto Descripción
options SearchableSelectOption[] Opciones
value string | string[] Selección. Array si multiple
onChange (value: string) => void | (value: string[]) => void Cambio de selección
multiple boolean false Selección múltiple con chips
label ReactNode Etiqueta, asociada por htmlFor
required boolean Marca visible y aria-required
placeholder string Texto con la selección vacía
helperText string Texto de ayuda
error string Mensaje de error
disabled boolean Desactiva el control
clearable boolean Botón X para deseleccionar
loading boolean Spinner en el listado
onSearchChange (term: string) => void Modo servidor: desactiva el filtrado en cliente
emptyMessage string Texto cuando no hay resultados
creatable boolean false Permite crear una opción inexistente
onCreate (term: string) => string | void | Promise<string | void> Crea la opción y devuelve el valor a seleccionar
createLabel string 'Crear «{term}»' Texto del botón de crear; {term} se sustituye
hasMore boolean Quedan más resultados por traer
onLoadMore () => void Se llama al llegar al final de la lista
highlightMatch boolean true Resalta en negrita lo que coincide
grouped boolean true si alguna opción trae group Ordena y agrupa por group
id string id del control
className string Clases
Prop Tipo Descripción
value string Valor
label string Etiqueta
secondaryLabel string Segunda línea, más tenue
disabled boolean Desactiva la opción
group string Encabezado bajo el que se agrupa
depth number Nivel de anidamiento, para listas jerárquicas
icon ReactNode Icono a la izquierda

Acepta claves extra: puedes colgar del objeto lo que necesites para tu onChange.

NumberInput es un campo numérico que no te pelea mientras escribes: los estados intermedios como 0, o - no se normalizan hasta que terminas, que es lo que rompía en las implementaciones a mano. Acepta coma y punto como separador decimal.

import { NumberInput, CurrencyInput, PercentInput } from '@openfactu/ui';
<NumberInput label="Unidades" value={unidades} onChange={setUnidades} min={0} showSteppers />
<NumberInput
label="Peso"
value={peso}
onChange={setPeso}
precision={3}
suffix="kg"
helperText="Se puede teclear «2,5» o «2.5»."
/>

value es number | null, no una cadena: el campo vacío es null (o 0, si pones emptyValue="zero").

Son NumberInput con los valores por defecto puestos: CurrencyInput trae dos decimales, separador de millares y el símbolo detrás; PercentInput, dos decimales, sufijo % y el rango 0–100.

<CurrencyInput label="Precio" value={precio} onChange={setPrecio} />
<CurrencyInput label="En dólares" currency="USD" symbolPosition="prefix" value={precio} onChange={setPrecio} />
<PercentInput label="IVA" value={iva} onChange={setIva} />

Si el porcentaje viaja de 0 a 1 en tu modelo pero se enseña de 0 a 100, pon fractional.

Hereda las de Input salvo value, defaultValue, onChange y type.

Prop Tipo Por defecto Descripción
value number | null Valor
onChange (value: number | null) => void Cambio de valor
commitOn 'change' | 'blur' 'change' change avisa en cada tecla; blur, al salir
min / max number Límites
step number 1 Salto de los botones ▲▼
showSteppers boolean false Botones ▲▼ como sufijo
precision number 0 Decimales al dar formato
decimalSeparator ',' | '.' ',' Separador decimal al formatear
thousandSeparator string | false false Separador de millares
allowNegative boolean true Permite negativos
emptyValue 'null' | 'zero' 'null' Qué representa el campo vacío
align 'left' | 'right' 'right' Alineación del texto
clampOnBlur boolean true Recorta a [min, max] al salir
selectOnFocus boolean true Selecciona el contenido al enfocar
Prop Tipo Por defecto Descripción
currency string 'EUR' Código ISO; elige el símbolo
symbolPosition 'prefix' | 'suffix' 'suffix' Dónde va el símbolo

Cambia además los valores por defecto heredados: precision a 2, thousandSeparator a '.' y allowNegative a false.

Prop Tipo Por defecto Descripción
fractional boolean false El valor va de 0 a 1 y se muestra de 0 a 100

Con precision a 2, min a 0, max a 100, allowNegative a false y sufijo %.

Campo de contraseña con botón de mostrar/ocultar, y opcionalmente barra de fortaleza, generador y lista de requisitos.

import { PasswordInput } from '@openfactu/ui';
<PasswordInput label="Contraseña" value={clave} onChange={(e) => setClave(e.target.value)} />
<PasswordInput
label="Nueva contraseña"
value={clave}
onChange={(e) => setClave(e.target.value)}
showStrength
generator
/>

Cada requisito se marca en verde en cuanto su test pasa.

<PasswordInput
label="Contraseña"
value={clave}
onChange={(e) => setClave(e.target.value)}
requirements={[
{ label: 'Al menos 8 caracteres', test: (v) => v.length >= 8 },
{ label: 'Una mayúscula', test: (v) => /[A-Z]/.test(v) },
{ label: 'Un número', test: (v) => /\d/.test(v) },
]}
/>

Hereda las de Input salvo type y suffix.

Prop Tipo Por defecto Descripción
toggleVisibility boolean true Botón de mostrar/ocultar
visible boolean Visibilidad controlada
onVisibleChange (visible: boolean) => void Cambio de visibilidad
showStrength boolean Barra de fortaleza bajo el campo
strengthLabels [string, string, string, string] Textos de los cuatro niveles
generator boolean | { length?: number; onGenerate?: (value: string) => void } Botón para generar una contraseña
requirements PasswordRequirement[] Lista de requisitos: { label, test }

Campo de búsqueda con debounce, atajo de teclado, historial, sugerencias agrupadas y filtros por token. Para el buscador global de la aplicación, mira SearchTrigger y CommandPalette.

import { SearchInput } from '@openfactu/ui';
<SearchInput value={texto} onChange={setTexto} placeholder="Filtrar artículos…" />

onChange avisa en cada tecla; onDebouncedChange solo cuando el usuario para de escribir. Consulta el servidor con el segundo, no con el primero.

<SearchInput
value={texto}
onChange={setTexto}
onDebouncedChange={consultar}
debounceMs={400}
shortcut="mod+k"
placeholder="Buscar en toda la aplicación…"
/>
<SearchInput
value={texto}
onChange={setTexto}
suggestions={[
{ id: '1', label: 'FAC/2026/0042', description: 'Acme S.L.', group: 'Documentos' },
{ id: '2', label: 'Acme S.L.', description: 'B12345678', group: 'Clientes' },
]}
recentKey="busquedas-facturas"
tokens={tokens}
onTokensChange={setTokens}
onSubmit={(v) => buscar(v)}
placeholder="Buscar documentos y clientes…"
/>

Con el campo vacío y enfocado se ve el historial; al escribir, las sugerencias agrupadas. Los tokens se pintan como chips delante del campo y se quitan uno a uno.

El analizador de tokens, suelto, por si quieres interpretar estado:pagada cliente:acme por tu cuenta:

import { parseSearchTokens } from '@openfactu/ui';
parseSearchTokens('estado:pagada cliente:acme factura de julio');
// {
// tokens: [{ field: 'estado', value: 'pagada' }, { field: 'cliente', value: 'acme' }],
// rest: 'factura de julio',
// }

Hereda las de Input salvo type, leftIcon, value, onChange, prefix y onSubmit.

Prop Tipo Por defecto Descripción
value string Texto
onChange (value: string) => void Cambio en cada tecla
onDebouncedChange (value: string) => void Cambio cuando deja de teclear
debounceMs number 250 Espera del debounce
clearable boolean true Botón X para vaciar
onClear () => void Se llama al vaciar
onSubmit (value: string) => void Se llama al pulsar Intro
shortcut 'mod+k' | 'ctrl+k' | '/' | false false Atajo global que enfoca el campo
showShortcutHint boolean Pinta la tecla del atajo dentro del campo
loading boolean false Spinner
trailing ReactNode Contenido extra a la derecha
bare boolean false Sin borde ni fondo, para barras de herramientas
suggestions SearchSuggestion[] Sugerencias en desplegable
onSuggestionSelect (s: SearchSuggestion) => void Selección de una sugerencia
recentKey string | false false Clave de almacenamiento del historial
maxRecent number 5 Entradas de historial que se guardan
tokens SearchToken[] Filtros por token, como chips
onTokensChange (tokens: SearchToken[]) => void Cambio de los tokens
placeholder string 'Buscar…' Texto de marcador

SearchSuggestion es { id, label, description?, icon?, group? } y SearchToken es { field, value, label? }.

Casilla con etiqueta asociada, de forma que también se marca pulsando el texto. Soporta el estado indeterminado, que es el que necesita la cabecera de una tabla con selección parcial.

import { Checkbox } from '@openfactu/ui';
<Checkbox checked={recargo} onChange={setRecargo} label="Recargo de equivalencia" />
<Checkbox
checked={copia}
onChange={setCopia}
label="Enviar copia por correo"
description="Se manda al contacto principal en cuanto se valide la factura."
/>
<Checkbox checked={valor} onChange={setValor} size="sm" label="Etiqueta pequeña" required />
<Checkbox
checked={todas}
state={ninguna ? 'unchecked' : todas ? 'checked' : 'indeterminate'}
onChange={alternarTodas}
label="Seleccionar todo"
/>
Prop Tipo Por defecto Descripción
checked boolean Marcada
state 'checked' | 'unchecked' | 'indeterminate' Tristate: indeterminate para selección parcial
onChange (checked: boolean) => void Cambio de valor
size 'sm' | 'md' 'md' Tamaño
label ReactNode Texto a la derecha, unido por htmlFor
description ReactNode Segunda línea bajo la etiqueta
labelClassName / containerClassName string Clases
…resto InputHTMLAttributes sin onChange, type ni size required, disabled, name

Interruptor para ajustes que se aplican al momento. Si el cambio necesita un «Guardar», usa un Checkbox.

import { Switch } from '@openfactu/ui';
<Switch checked={activo} onChange={setActivo} label="Cliente activo" />
<Switch checked={avisos} onChange={setAvisos} size="sm" label="Avisos por correo" />
Prop Tipo Por defecto Descripción
checked boolean Encendido
onChange (checked: boolean) => void Cambio de valor
label string Etiqueta a la derecha
size 'sm' | 'md' 'md' Tamaño
disabled boolean Desactiva el control
id string id del control
className string Clases

Opción única entre pocas alternativas, cuando conviene verlas todas a la vez.

import { RadioGroup } from '@openfactu/ui';
<RadioGroup
label="Tipo de documento"
value={tipo}
onChange={setTipo}
options={[
{ value: 'factura', label: 'Factura', description: 'Documento definitivo, con numeración de serie.' },
{ value: 'proforma', label: 'Proforma', description: 'Sin validez fiscal.' },
{ value: 'presupuesto', label: 'Presupuesto', disabled: true },
]}
/>
<RadioGroup
label="Periodicidad"
orientation="horizontal"
value={periodo}
onChange={setPeriodo}
options={[
{ value: 'mensual', label: 'Mensual' },
{ value: 'trimestral', label: 'Trimestral' },
]}
/>
Prop Tipo Por defecto Descripción
options RadioOption[] Opciones: { value, label, description?, disabled? }
value string Opción seleccionada
onChange (value: string) => void Cambio de selección
name string useId() name compartido de los inputs
label string Etiqueta del grupo
error string Mensaje de error
orientation 'vertical' | 'horizontal' 'vertical' Disposición
disabled boolean Desactiva el grupo entero
className string Clases

Selector de fecha con calendario propio. El valor viaja en ISO YYYY-MM-DD, que es como lo esperan la API y la base de datos; lo que se enseña es dd/mm/aaaa.

import { DatePicker } from '@openfactu/ui';
<DatePicker label="Fecha de emisión" required value={fecha} onChange={setFecha} />

onChange recibe string | null: null cuando se limpia el campo.

<DatePicker
label="Fecha de vencimiento"
value={vencimiento}
onChange={setVencimiento}
min={emision}
max="2026-12-31"
clearable
helperText="No puede ser anterior a la emisión."
/>
{/* Para fechas lejanas, abrir directamente en la vista de años */}
<DatePicker label="Fecha de nacimiento" initialView="years" value={nacimiento} onChange={setNacimiento} />

El calendario tiene tres vistas —días, meses y años— y se navega entre ellas pulsando la cabecera.

<DatePicker
value={fecha}
onChange={setFecha}
weekStartsOn={0}
locale={{
months: ['January', 'February', ''],
weekdays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
today: 'Today',
clear: 'Clear',
}}
/>
Prop Tipo Por defecto Descripción
value string | null Fecha en ISO YYYY-MM-DD
onChange (value: string | null) => void Cambio de fecha
label string Etiqueta
required boolean Marca visible y aria-required
error string Mensaje de error
helperText string Texto de ayuda
placeholder string 'dd/mm/aaaa' Texto de marcador
min / max string Límites en ISO, inclusive
disabled boolean false Desactiva el control
clearable boolean false Botón para limpiar
initialView 'days' | 'months' | 'years' 'days' Vista con la que abre
yearRange [number, number] derivado de min/max Años navegables
weekStartsOn 0 | 1 1 0 domingo, 1 lunes
locale Partial<DatePickerLocale> español Sustituye los literales
hideFooter boolean false Oculta el pie con «Hoy» y «Limpiar»
onViewChange (year: number, month: number) => void Cambia el mes/año visible, no la selección
className string Clases

Muestra de color, campo hexadecimal y, si quieres, una paleta de acceso rápido.

import { ColorInput } from '@openfactu/ui';
<ColorInput label="Color de marca" value={color} onChange={setColor} />
<ColorInput
label="Color de marca"
value={color}
onChange={setColor}
clearable
presets={['#0a1628', '#0d9488', '#1e293b', '#18181b', '#1e102c']}
helperText="Elige uno de la paleta o escribe el hexadecimal."
/>
Prop Tipo Por defecto Descripción
value string Color en #rrggbb, o cadena vacía
onChange (value: string) => void Cambio de color
label ReactNode Etiqueta
error string Mensaje de error
helperText ReactNode Texto de ayuda
disabled boolean false Desactiva el control
showHex boolean true Campo de texto con el hex junto a la muestra
presets string[] Paleta de acceso rápido bajo el campo
clearable boolean false Permite dejarlo vacío
size 'sm' | 'md' | 'lg' 'md' Tamaño
id / name string Atributos del control
className / containerClassName string Clases

Zona de arrastrar y soltar, con validación de tipo, tamaño y número de archivos.

import { FileDropzone } from '@openfactu/ui';
<FileDropzone
multiple
accept="image/*,.pdf"
maxSizeMb={10}
hint="PNG, JPG o PDF · máx. 10 MB"
files={archivos}
onFiles={(f) => setArchivos((prev) => [...prev, ...f])}
onRemoveFile={(i) => setArchivos((prev) => prev.filter((_, idx) => idx !== i))}
onReject={(motivo, rechazados) => avisar(motivo, rechazados)}
/>

onReject te dice por qué se ha rechazado —'size', 'type' o 'count'— y con qué archivos, para que puedas dar un mensaje concreto en vez de un «error» genérico.

<FileDropzone onFiles={subir} isUploading progress={62} hint="No cierres esta ventana." />

area es el recuadro grande; inline una línea discreta; button un simple botón.

<FileDropzone variant="inline" onFiles={subir} label="Adjuntar documento" />
<FileDropzone variant="button" onFiles={subir} label="Subir archivo" />
<FileDropzone onFiles={subir} error="El archivo supera el tamaño permitido." />
Prop Tipo Por defecto Descripción
onFiles (files: File[]) => void | Promise<void> Archivos aceptados
accept string Igual que el atributo accept nativo
multiple boolean false Permite varios archivos
maxSizeMb number Tamaño máximo por archivo
maxFiles number Número máximo de archivos
disabled boolean false Desactiva la zona
isUploading boolean false Estado de subida en curso
uploadingLabel ReactNode 'Subiendo…' Texto durante la subida
progress number 0–100; dibuja una barra de progreso
label ReactNode 'Arrastra archivos o haz clic para seleccionar' Texto principal
hint ReactNode Segunda línea con las restricciones
icon ReactNode Icono del área
variant 'area' | 'inline' | 'button' 'area' Forma del control
acceptPaste boolean false Acepta pegar archivos del portapapeles
files File[] Archivos ya seleccionados, con opción de quitarlos
onRemoveFile (index: number, file: File) => void Quitar un archivo de la lista
onReject (reason: FileRejectReason, files: File[]) => void Rechazo por 'size', 'type' o 'count'
error string Mensaje de error
children ReactNode Sustituye el contenido del área conservando el arrastrar y soltar
className string Clases