Ir al contenido

Feedback y estado

Cómo se le cuenta al usuario que algo está pasando: cargando, avanzando, vacío o terminado.

El spinner suelto.

import { Loader } from '@openfactu/ui';
<Loader />
<Loader size="lg" label="Cargando facturas…" />
<Loader variant="white" />
<Loader overlay label="Guardando…" />

overlay lo pinta sobre una capa que cubre al contenedor: sirve para bloquear una tarjeta mientras se guarda, sin desmontar lo que hay debajo.

Prop Tipo Por defecto Descripción
size 'sm' | 'md' | 'lg' | 'xl' 'md' Tamaño
variant 'primary' | 'white' | 'neutral' 'primary' Color
label string Texto bajo el spinner
overlay boolean Capa que cubre al contenedor
…resto HTMLAttributes<HTMLDivElement> className

La pantalla de carga de toda la aplicación.

import { GlobalLoader } from '@openfactu/ui';
<GlobalLoader isLoading={arrancando} message="Cargando tu empresa…" />
Prop Tipo Descripción
isLoading boolean Si se muestra
message string Texto bajo el indicador

La barra fantasma. Sirve suelta o en cualquiera de las cuatro composiciones que trae hechas.

import { Skeleton } from '@openfactu/ui';
<Skeleton /> {/* una línea de texto */}
<Skeleton lines={3} /> {/* tres líneas; la última al 65 % */}
<Skeleton variant="rect" height={120} />
<Skeleton variant="circle" width={32} height={32} />
<Skeleton animation="shimmer" delayMs={80} />

delayMs retrasa la animación, que es como se escalonan varias barras para que no parpadeen todas a la vez.

Prop Tipo Por defecto Descripción
variant 'text' | 'rect' | 'circle' 'text' text añade alto de línea; circle fuerza radio completo
width / height number | string Dimensiones
lines number Solo en text: líneas apiladas
lastLineWidth number | string '65%' Ancho de la última línea
gap number | string 8 Separación entre líneas
radius 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'full' | string Redondez
animation 'pulse' | 'shimmer' | 'none' 'pulse' Animación
delayMs number Retardo, para escalonar varias barras
count number 1 Obsoleta: usa lines
import { SkeletonCard, SkeletonList, SkeletonTable, SkeletonPage, DashboardSkeleton } from '@openfactu/ui';
<SkeletonCard showAvatar showFooter />
<SkeletonList rows={8} showActions />
<SkeletonTable rows={10} columns={5} density="compact" />
<SkeletonPage header kpis={4} columns={12} blocks={3} />
<DashboardSkeleton />

DashboardSkeleton no lleva props: es un SkeletonPage con cuatro KPIs y cuatro bloques, centrado, tal como está el panel de inicio.

Prop Tipo Por defecto
count number 1
lines number 2
showHeader boolean true
showAvatar boolean false
showFooter boolean false
animation SkeletonAnimation
Prop Tipo Por defecto
rows number 5
showAvatar boolean true
showSubtitle boolean true
showMeta boolean true
showActions boolean false
density 'compact' | 'normal' | 'comfy' 'normal'
variant 'plain' | 'divided' | 'bordered' 'divided'
Prop Tipo Por defecto Descripción
rows number 6 Filas fantasma
columns number | Array<{ width?, align? }> 4 Número de columnas, o especificación por columna
density 'compact' | 'normal' | 'comfy' 'normal' Densidad
showHeader boolean true Cabecera falsa. Table lo pone en false: usa la real
Prop Tipo Por defecto Descripción
header boolean true Título y subtítulo arriba
kpis number 0 Tarjetas KPI
columns 1 | 2 | 12 1 Distribución del cuerpo
blocks number 2 Bloques de contenido

Barra de progreso horizontal.

import { Progress } from '@openfactu/ui';
<Progress value={62} />
<Progress value={62} label="Subiendo adjuntos" showValue />
<Progress value={95} variant="warning" size="sm" />
<Progress value={100} variant="success" label="Importación terminada" showValue />
Prop Tipo Por defecto Descripción
value number 0–100; se recorta por dentro
variant 'accent' | 'success' | 'warning' | 'danger' 'accent' Color
size 'sm' | 'md' 'md' Grosor
label string Etiqueta encima
showValue boolean false Porcentaje a la derecha del label
className string Clases

El anillo de progreso, para consumos y cuotas. Es SVG propio: no arrastra ninguna librería de gráficos.

import { Ring } from '@openfactu/ui';
<Ring value={72} label />
<Ring value={1840} max={2000} size={64} thickness={6} label="92 %" ariaLabel="Almacenamiento usado" />
<Ring value={95} tone="auto" label />
Prop Tipo Por defecto Descripción
value number Valor
max number 100 Máximo
size number 40 Diámetro en píxeles
thickness number 4 Grosor del anillo
tone 'accent' | 'success' | 'warning' | 'danger' | 'auto' 'accent' Color, o umbral automático
label ReactNode | true Contenido central; true muestra el porcentaje
ariaLabel string Descripción accesible del valor
className string Clases

El estado vacío con icono, título, pista y acción.

import { EmptyState, Button } from '@openfactu/ui';
import { Inbox, Plus } from 'lucide-react';
<EmptyState
icon={<Inbox className="h-8 w-8" />}
title="Todavía no hay facturas"
hint="Cuando emitas la primera aparecerá aquí."
action={
<Button variant="accent">
<Plus className="h-3.5 w-3.5" /> Nueva factura
</Button>
}
/>

List lo acepta directamente en su prop emptyState, pasándole estas mismas props sin montar el componente.

Prop Tipo Descripción
title string Título
icon ReactNode Icono
hint string Texto de ayuda
action ReactNode Normalmente un <Button>
className string Clases

Los avisos. Se usan a través de useToast, con el ToastProvider montado una vez en la raíz de la aplicación.

import { ToastProvider } from '@openfactu/ui';
<ToastProvider position="bottom-right" max={3}>
<App />
</ToastProvider>
import { useToast } from '@openfactu/ui';
function BotonGuardar() {
const toast = useToast();
const guardar = async () => {
try {
await guardarFactura();
toast.success('Factura guardada');
} catch (e) {
toast.error('No se pudo guardar');
}
};
return <Button onClick={guardar}>Guardar</Button>;
}

Lo más cómodo para una operación con espera: enseña «cargando» y al terminar lo cambia por el mensaje de éxito o de error en el mismo aviso, sin apilar otro.

toast.promise(guardarFactura(), {
loading: 'Guardando…',
success: 'Factura guardada',
error: (e) => `No se pudo guardar: ${e.message}`,
});
toast.success('Factura eliminada', {
action: { label: 'Deshacer', onClick: restaurar },
});
toast.warning('Hay cambios sin guardar', { duration: Infinity });
const id = toast.loading('Sincronizando con la AEAT…');
// …más tarde
toast.update(id, { type: 'success', message: 'Sincronizado' });

duration: 0 o Infinity dejan el aviso fijo hasta que se cierre a mano. Los de tipo loading ya no se cierran solos.

toast.info('Aviso discreto', { variant: 'outline' });
toast.error('Esto tiene que cantar', { variant: 'solid', animation: 'lift' });
Variante Aspecto
accent Tarjeta neutra con banda de color a la izquierda. El de siempre
soft Fondo teñido del color de estado, para avisos que deben cantar más
solid Relleno del color de estado. El más llamativo
outline Solo contorno. El más discreto

Animaciones: slide, fade, scale y lift. La salida es la misma transición al revés.

Método Firma Descripción
success (message, options?) => string Aviso de éxito
error (message, options?) => string Aviso de error
info (message, options?) => string Aviso informativo
warning (message, options?) => string Aviso de advertencia
loading (message, options?) => string Con spinner; no se cierra solo
show (message, options & { type? }) => string Control total
dismiss (id?) => void Cierra uno, o todos si no se indica
update (id, patch) => void Actualiza un aviso ya visible
promise (promise, { loading, success, error }, options?) => Promise<T> Sigue una promesa

Todos devuelven el id del aviso, que es lo que necesitas para update y dismiss.

Prop Tipo Descripción
title ReactNode Encabezado sobre el mensaje
variant ToastVariant Forma. Por defecto la del proveedor
animation ToastAnimation Entrada y salida. Por defecto la del proveedor
duration number Milisegundos. 0 o Infinity lo dejan fijo
action { label, onClick } Botón a la derecha
dismissible boolean Se puede cerrar a mano
id string Identificador propio; repetirlo sustituye el aviso
Prop Tipo Por defecto Descripción
children ReactNode La aplicación
position ToastPosition 'top-right' Esquina donde se apilan
max number 4 Cuántos se ven a la vez; el resto espera turno
duration number 5000 Duración por defecto en milisegundos
variant ToastVariant 'accent' Forma por defecto
animation ToastAnimation 'slide' Animación por defecto
swipeToDismiss boolean true Descartar deslizando con el dedo
container HTMLElement | null document.body Dónde se monta el portal

Posiciones: top-right, top-left, top-center, bottom-right, bottom-left y bottom-center.

El temporizador se detiene mientras el ratón está encima de la pila, así que un aviso no desaparece justo cuando ibas a pulsar su acción.

Monta y desmonta un nodo con transición de entrada y de salida, esperando a que termine antes de quitarlo del árbol.

import { Transition } from '@openfactu/ui';
<Transition show={abierto} from="opacity-0 -translate-y-2" to="opacity-100 translate-y-0">
<div className="rounded border p-4">Panel de detalle</div>
</Transition>
<Transition show={visible} from="opacity-0 scale-95" to="opacity-100 scale-100" duration={150}>
<Aviso />
</Transition>

Con unmountOnExit={false} el contenido se queda montado y solo se oculta, que es lo que quieres si dentro hay estado que no debe perderse.

Prop Tipo Por defecto Descripción
show boolean true anima la entrada; false, la salida y desmonta
children ReactNode Contenido
from string 'opacity-0' Clases del estado oculto
to string 'opacity-100' Clases del estado visible
duration number 200 Duración en milisegundos
easing CSSProperties['transitionTimingFunction'] 'ease-out' Curva
unmountOnExit boolean true Si false, queda montado y solo se oculta
className string Clases