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.
El andamiaje
Sección titulada «El andamiaje»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>Tamaños y orientación
Sección titulada «Tamaños y orientación»<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, FormGrid y FormRow
Sección titulada «FormSection, FormGrid y FormRow»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.
FormSection
Sección titulada «FormSection»| 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 |
FormGrid
Sección titulada «FormGrid»| 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 |
FormRow
Sección titulada «FormRow»| 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" />Iconos y complementos
Sección titulada «Iconos y complementos»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.
Validación
Sección titulada «Validación»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… |
Textarea
Sección titulada «Textarea»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 |
SearchableSelect
Sección titulada «SearchableSelect»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."/>Selección múltiple
Sección titulada «Selección múltiple»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} />Agrupado y jerárquico
Sección titulada «Agrupado y jerárquico»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 }, ]}/>Búsqueda en servidor
Sección titulada «Búsqueda en servidor»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.
Crear una opción que no existe
Sección titulada «Crear una opción que no existe»<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 |
SearchableSelectOption
Sección titulada «SearchableSelectOption»| 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, CurrencyInput y PercentInput
Sección titulada «NumberInput, CurrencyInput y PercentInput»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").
Moneda y porcentaje
Sección titulada «Moneda y porcentaje»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.
Props de NumberInput
Sección titulada «Props de NumberInput»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 |
CurrencyInput
Sección titulada «CurrencyInput»| 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.
PercentInput
Sección titulada «PercentInput»| 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 %.
PasswordInput
Sección titulada «PasswordInput»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)} />Con fortaleza y generador
Sección titulada «Con fortaleza y generador»<PasswordInput label="Nueva contraseña" value={clave} onChange={(e) => setClave(e.target.value)} showStrength generator/>Con requisitos
Sección titulada «Con requisitos»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 } |
SearchInput
Sección titulada «SearchInput»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…" />Debounce y atajo
Sección titulada «Debounce y atajo»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…"/>Sugerencias, historial y filtros
Sección titulada «Sugerencias, historial y filtros»<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.
parseSearchTokens
Sección titulada «parseSearchTokens»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? }.
Checkbox
Sección titulada «Checkbox»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 />Estado indeterminado
Sección titulada «Estado indeterminado»<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 |
RadioGroup
Sección titulada «RadioGroup»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 |
DatePicker
Sección titulada «DatePicker»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.
Límites y navegación
Sección titulada «Límites y navegación»<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.
Otro idioma o semana en domingo
Sección titulada «Otro idioma o semana en domingo»<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 |
ColorInput
Sección titulada «ColorInput»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 |
FileDropzone
Sección titulada «FileDropzone»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.
Durante la subida
Sección titulada «Durante la subida»<FileDropzone onFiles={subir} isUploading progress={62} hint="No cierres esta ventana." />Variantes
Sección titulada «Variantes»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 |