Datos y tablas
Los componentes con los que se enseñan datos: la tabla completa, el editor de líneas, la lista, y las piezas sueltas que los acompañan.
La tabla del ERP. Ordenación, selección múltiple, paginación de cliente o de servidor, scroll infinito, filas expandibles, acciones de fila, columnas fijas, redimensionables y ocultables, esqueleto de carga, fila de totales y modo tarjetas en móvil.
import { Table, Badge, type TableColumn } from '@openfactu/ui';
interface Factura { id: number; numero: string; cliente: string; total: number; estado: 'borrador' | 'contabilizada' | 'pagada';}
const ESTADO = { borrador: <Badge variant="neutral">Borrador</Badge>, contabilizada: <Badge variant="info">Contabilizada</Badge>, pagada: <Badge variant="success">Pagada</Badge>,};
const columnas: TableColumn<Factura>[] = [ { header: 'Número', accessor: 'numero', primary: true, sortable: true }, { header: 'Cliente', accessor: 'cliente', sortable: true }, { header: 'Estado', cell: (f) => ESTADO[f.estado], align: 'center' }, { header: 'Total', accessor: (f) => `${f.total.toFixed(2)} €`, align: 'right', sortable: true, sortAccessor: (f) => f.total, },];
<Table columns={columnas} data={facturas} onRowClick={(f) => abrir(f)} />Columnas
Sección titulada «Columnas»accessor puede ser una clave del objeto o una función. cell es lo mismo pero solo
para pintar. primary marca la columna identificadora, que se pinta en monoespaciada.
Si dos columnas comparten header, dales un id: es la clave que se usa para
ordenación, visibilidad y redimensionado.
Selección
Sección titulada «Selección»const [seleccionadas, setSeleccionadas] = React.useState<Set<string | number>>(new Set());
<Table columns={columnas} data={facturas} selectable selectedKeys={seleccionadas} onSelectionChange={setSeleccionadas}/>Si omites selectedKeys, la tabla mantiene la selección por dentro. La clave de cada
fila sale de item.id; con otro nombre, pásale rowKey.
Se combina con BulkActionsBar para las acciones
masivas.
Paginación
Sección titulada «Paginación»{/* cliente: le pasas todas las filas y la tabla las trocea */}<Table columns={columnas} data={facturas} pagination={{ pageSize: 10 }} />
{/* servidor: `data` es solo la página actual y `total` el número real de registros */}<Table columns={columnas} data={pagina} pagination={{ pageSize: 25, page, total: totalRegistros, onPageChange: setPage, onPageSizeChange: setPageSize, }}/>Scroll infinito
Sección titulada «Scroll infinito»Alternativa a la paginación: carga la página siguiente al acercarse al final.
<Table columns={columnas} data={filas} infinite={{ hasMore, loading: cargando, onLoadMore: traerSiguientePagina, endMessage: 'No hay más facturas.', }}/>Acciones de fila
Sección titulada «Acciones de fila»Aparecen en un botón ⋯ al pasar el ratón, y también con click derecho sobre la fila. Usan el mismo shape de ítem de menú que el resto.
<Table columns={columnas} data={facturas} rowActions={(f) => [ { label: 'Editar', onClick: () => editar(f) }, { label: 'Duplicar', onClick: () => duplicar(f) }, { label: 'Eliminar', destructive: true, separatorBefore: true, onClick: () => eliminar(f) }, ]}/>Filas expandibles
Sección titulada «Filas expandibles»<Table columns={columnas} data={facturas} renderExpanded={(f) => ( <div className="flex gap-8 text-[12px]"> <p>{f.numero} · {f.cliente}</p> <p className="font-mono">{f.total.toFixed(2)} €</p> </div> )}/>Estado vacío y carga
Sección titulada «Estado vacío y carga»emptyMessage admite nodos, no solo texto: un vacío útil suele llevar icono y un
botón para crear el primer registro.
<Table columns={columnas} data={[]} emptyMessage={ <div className="flex flex-col items-center gap-2 py-4"> <Inbox className="h-6 w-6 text-[var(--fg-subtle)]" /> <p className="text-[13px] text-[var(--fg-muted)]">Todavía no hay facturas.</p> <Button size="sm" variant="accent"> <Plus className="h-3.5 w-3.5" /> Crear la primera </Button> </div> }/>
<Table columns={columnas} data={[]} isLoading /><Table columns={columnas} data={[]} isLoading loadingVariant="spinner" loadingLabel="Sincronizando…" />Columnas fijas, redimensionables y ocultables
Sección titulada «Columnas fijas, redimensionables y ocultables»<Table columns={columnas} data={facturas} selectable stickyFirstColumn resizableColumns showColumnToggle/>Fila de totales
Sección titulada «Fila de totales»Se le pasan las filas visibles y devuelve una celda por columna visible, así que el pie queda alineado con la tabla sin montar un bloque aparte.
<Table columns={columnas} data={facturas} summaryRow={(filas) => [ null, null, null, `${filas.reduce((a, f) => a + f.total, 0).toFixed(2)} €`, ]}/>En móvil: tarjetas
Sección titulada «En móvil: tarjetas»Por debajo del punto de corte la tabla pinta una tarjeta por fila en lugar de obligar
a desplazarse en horizontal. El papel de cada columna se declara con card; lo que no
lo lleve va al cuerpo de la tarjeta con su etiqueta delante.
<Table responsive="cards" cardsBreakpoint={640} columns={[ { header: 'Número', accessor: 'numero', primary: true, card: 'title' }, { header: 'Cliente', accessor: 'cliente', card: 'subtitle' }, { header: 'Estado', cell: (f) => ESTADO[f.estado], card: 'status' }, { header: 'Total', accessor: (f) => `${f.total.toFixed(2)} €`, align: 'right', card: 'meta' }, ]} data={facturas}/>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
columns |
TableColumn<T>[] |
— | Definición de columnas |
data |
T[] |
[] |
Filas |
onRowClick |
(item: T) => void |
— | Click en una fila |
emptyMessage |
ReactNode |
'No se encontraron registros.' |
Estado vacío |
rowClassName |
(item: T, index: number) => string | undefined |
— | Clases extra por fila |
isLoading |
boolean |
— | Estado de carga |
density |
'compact' | 'normal' | 'comfy' |
'normal' |
Densidad de filas |
selectable |
boolean |
false |
Columna de checkboxes y multiselección |
selectedKeys |
Set<string | number> |
— | Selección controlada |
onSelectionChange |
(keys: Set<string | number>) => void |
— | Cambio de selección |
rowKey |
(item: T, index: number) => string | number |
item.id |
Cómo extraer la clave de cada fila |
pagination |
TablePagination |
— | Paginación de cliente o de servidor |
infinite |
TableInfinite |
— | Scroll infinito. Excluyente con pagination |
renderExpanded |
(item: T) => ReactNode |
— | Contenido del detalle. Añade columna chevron |
expandedKeys |
Set<string | number> |
— | Expansión controlada |
onExpandedChange |
(keys: Set<string | number>) => void |
— | Cambio de expansión |
rowActions |
(item: T) => RowAction[] |
— | Acciones de fila (botón ⋯ y click derecho) |
stickyFirstColumn |
boolean |
false |
Fija la primera columna al hacer scroll horizontal |
columnVisibility |
Record<string, boolean> |
— | Visibilidad controlada; clave col.id ?? col.header |
onColumnVisibilityChange |
(v: Record<string, boolean>) => void |
— | Cambio de visibilidad |
showColumnToggle |
boolean |
false |
Botón para mostrar/ocultar columnas |
resizableColumns |
boolean |
false |
Redimensionar arrastrando el borde de la cabecera |
loadingVariant |
'skeleton' | 'spinner' |
'skeleton' |
Cómo se representa isLoading |
skeletonRows |
number |
tamaño de página, u 8 |
Filas fantasma |
skeletonRowHeight |
number | string |
1.25em |
Alto de la caja de cada celda fantasma |
loadingLabel |
string |
'Sincronizando…' |
Texto bajo el spinner |
responsive |
'scroll' | 'cards' |
'scroll' |
Qué hacer cuando no cabe |
cardsBreakpoint |
number |
640 |
Ancho por debajo del cual se pasa a tarjetas |
summaryRow |
(rows: T[]) => ReactNode[] |
— | Fila de totales al pie |
summaryLabel |
ReactNode |
'Total' |
Etiqueta de la primera celda del pie |
appendRow |
ReactNode |
— | Fila extra al final, para el «+ Añadir línea» |
className |
string |
— | Clases |
TableColumn<T>
Sección titulada «TableColumn<T>»| Prop | Tipo | Descripción |
|---|---|---|
header |
string |
Título de la columna |
accessor |
keyof T | ((item: T, index: number) => ReactNode) |
Valor de la celda |
cell |
(item: T, index: number) => ReactNode |
Renderizado de la celda |
align |
'left' | 'center' | 'right' |
Alineación |
width |
string |
Ancho fijo |
primary |
boolean |
Estilo de columna identificadora (monoespaciada) |
sortable |
boolean |
Columna ordenable |
sortAccessor |
(item: T) => any |
Clave de ordenación cuando accessor es función |
id |
string |
Clave estable. Obligatoria si dos columnas comparten header |
card |
'title' | 'subtitle' | 'status' | 'meta' | 'body' | 'hidden' |
Papel en el modo tarjetas |
className |
string |
Clases de la celda |
TablePagination
Sección titulada «TablePagination»| Prop | Tipo | Descripción |
|---|---|---|
pageSize |
number |
Filas por página |
page |
number |
Página controlada, 1-based |
onPageChange |
(page: number) => void |
Cambio de página |
total |
number |
Modo servidor: total real de registros |
pageSizeOptions |
number[] |
Opciones del selector de tamaño |
onPageSizeChange |
(size: number) => void |
Si se pasa, pageSize es controlado |
TableInfinite
Sección titulada «TableInfinite»| Prop | Tipo | Descripción |
|---|---|---|
hasMore |
boolean |
Quedan más filas por traer |
onLoadMore |
() => void |
Trae la página siguiente |
loading |
boolean |
Hay una petición en vuelo; no se pide otra |
endMessage |
ReactNode | false |
Qué poner al llegar al final |
EditableTable
Sección titulada «EditableTable»Table con edición dentro. Los controles se declaran, no se pintan: cada columna
dice qué editor quiere y el componente monta el resto.
Editor de líneas, celda a celda
Sección titulada «Editor de líneas, celda a celda»El modo de las líneas de un documento de venta: cada celda escribe directamente, sin botones de guardar.
import { EditableTable, Card, type EditableColumn } from '@openfactu/ui';
const columnas: EditableColumn<Linea>[] = [ { header: 'Artículo', accessor: (l) => ARTICULOS.find((a) => a.value === l.articulo)?.label ?? l.articulo, field: 'articulo', editor: { type: 'search-select', options: ARTICULOS, placeholder: 'Buscar artículo…' }, isDisabled: (l) => !!l.origen, }, { header: 'Cantidad', accessor: 'cantidad', align: 'right', width: '124px', editor: { type: 'number', precision: 2, min: 0 }, }, { header: 'Precio', accessor: (l) => money(l.precio), field: 'precio', align: 'right', editor: { type: 'currency' } }, { header: 'Dto.', accessor: (l) => `${l.descuento} %`, field: 'descuento', align: 'right', editor: { type: 'percent' } }, { header: 'Importe', accessor: (l) => money(total(l)), align: 'right' },];
<Card title="Líneas del documento" noPadding> <EditableTable mode="cell" density="compact" columns={columnas} data={lineas} onChange={(fila) => setLineas((prev) => prev.map((l) => (l.id === fila.id ? fila : l)))} onDelete={(fila) => setLineas((prev) => prev.filter((l) => l.id !== fila.id))} onAddRow={() => { const nueva = { id: Date.now(), articulo: '', cantidad: 1, precio: 0, descuento: 0 }; setLineas((prev) => [...prev, nueva]); return nueva; }} summaryRow={() => [null, null, null, null, money(suma)]} /></Card>isDisabled bloquea la celda en algunas filas: lo que hace falta para las líneas que
vienen de un pedido y no se tocan.
Edición por filas
Sección titulada «Edición por filas»En mode="row" la fila entera entra en edición con Guardar y Cancelar. Intro guarda
y Escape cancela.
<EditableTable columns={[ { header: 'Código', accessor: 'codigo', primary: true, width: '120px' }, { header: 'Nombre', accessor: 'nombre', editor: { type: 'text' } }, { header: 'Tipo', accessor: (i) => `${i.tipo} %`, field: 'tipo', align: 'right', editor: { type: 'percent' } }, ]} data={impuestos} onSave={(fila) => setImpuestos((prev) => prev.map((r) => (r.id === fila.id ? fila : r)))}/>Las columnas sin editor siguen siendo de solo lectura también mientras se edita la
fila, que es como se protege el código de un registro.
Tipos de editor
Sección titulada «Tipos de editor»text · number · currency · percent · select · search-select · date ·
checkbox · custom.
Con custom montas el control tú:
{ header: 'Responsable', field: 'responsable', editor: { type: 'custom', render: ({ value, onChange, row, disabled }) => ( <MiSelectorDeEmpleados value={value} onChange={onChange} disabled={disabled} /> ), },}Hereda todas las de Table salvo columns y rowActions.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
columns |
EditableColumn<T>[] |
— | Columnas, con su editor |
mode |
'row' | 'cell' |
'row' |
Fila entera con botones, o celda a celda |
onSave |
(row: T, original: T) => void | Promise<void> |
— | Confirmación de la edición de una fila |
onChange |
(row: T, field: string, value: any) => void |
— | En mode="cell", cada cambio |
onDelete |
(row: T) => void | Promise<void> |
— | Eliminar una fila |
onAddRow |
() => T |
— | Muestra la fila «añadir» y devuelve la fila nueva |
addLabel |
string |
— | Texto de la fila de añadir |
isRowEditable |
(row: T) => boolean |
— | Qué filas se pueden editar |
editingId |
string | number | null |
— | Fila que arranca en edición |
onEditingIdChange |
(id: string | number | null) => void |
— | Cambio de la fila en edición |
EditableColumn<T>
Sección titulada «EditableColumn<T>»Todo lo de TableColumn<T>, más:
| Prop | Tipo | Descripción |
|---|---|---|
editor |
CellEditor<T> |
Hace la columna editable |
field |
keyof T & string |
Campo que se escribe. Por defecto, accessor si es una clave |
isDisabled |
(row: T) => boolean |
Bloquea la celda en algunas filas |
CellEditor<T>
Sección titulada «CellEditor<T>»| Prop | Tipo | Descripción |
|---|---|---|
type |
CellEditorType |
Tipo de editor |
options |
SearchableSelectOption[] |
Opciones de select y search-select |
precision |
number |
Decimales de number, currency y percent |
min / max |
number |
Límites numéricos |
placeholder |
string |
Texto de marcador |
currency |
string |
Divisa del editor currency |
render |
(args) => ReactNode |
Control propio cuando type es 'custom' |
Lista de elementos con icono, título, subtítulo, metadatos, acciones y estado. Es la
alternativa a Table cuando cada elemento es una ficha y no una fila de datos:
adjuntos, notificaciones, conversaciones, historial.
import { List } from '@openfactu/ui';import { Trash2, MessageSquare } from 'lucide-react';
<List variant="plain" activeId={activa} items={[ { id: 'c1', icon: <MessageSquare className="h-4 w-4" />, title: 'Presupuesto Acme', onClick: () => setActiva('c1') }, { id: 'c2', icon: <MessageSquare className="h-4 w-4" />, title: 'Incidencia de envío', onClick: () => setActiva('c2') }, ]}/>Elementos enriquecidos
Sección titulada «Elementos enriquecidos»<List variant="bordered" items={[ { id: 1, icon: <FileText className="h-4 w-4" />, title: 'FAC-2026-0042.pdf', subtitle: 'Subido por Ana Pérez', meta: '248 KB', badge: <Badge variant="success">Firmado</Badge>, status: { tone: 'success', label: 'Validado' }, actions: [ { icon: <Download className="h-3.5 w-3.5" />, label: 'Descargar', onClick: descargar }, { icon: <Trash2 className="h-3.5 w-3.5" />, label: 'Eliminar', tone: 'danger', onClick: borrar }, ], }, ]}/>label es obligatorio en cada acción: los botones son solo icono, así que es su
nombre accesible.
Por secciones
Sección titulada «Por secciones»<List sections={[ { key: 'hoy', label: 'Hoy', meta: '3', collapsible: true, items: [/* … */], }, { key: 'anteriores', label: 'Anteriores', collapsible: true, defaultOpen: false, items: [/* … */] }, ]}/>Selección, carga y scroll infinito
Sección titulada «Selección, carga y scroll infinito»<List selectable selectedIds={seleccionados} onSelectionChange={setSeleccionados} items={elementos}/>
<List isLoading skeletonCount={5} items={[]} />
<List items={elementos} emptyState={{ title: 'Sin adjuntos', description: 'Arrastra un archivo para empezar.' }} infinite={{ hasMore, onLoadMore: traerMas, loading: cargando }}/>emptyState acepta un nodo propio o directamente las props del
EmptyState de la librería.
API de composición
Sección titulada «API de composición»<List variant="divided"> <List.Item title="Alta de proveedor" subtitle="Talleres del Norte" meta="hace 2 h" /> <List.Item title="Cierre de inventario" subtitle="Almacén central" meta="ayer" unread /></List>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
items |
ListItemData[] |
— | Modo plano. Excluyente con sections |
sections |
ListSectionData[] |
— | Modo agrupado por secciones |
density |
'compact' | 'normal' | 'comfy' |
'normal' |
Densidad |
variant |
'plain' | 'divided' | 'bordered' | 'cards' |
'divided' |
Estilo de la lista |
activeId |
string | number | null |
— | Elemento resaltado (navegación) |
selectable |
boolean |
false |
Casillas de selección múltiple |
selectedIds |
Array<string | number> |
— | Selección controlada |
onSelectionChange |
(ids: Array<string | number>) => void |
— | Cambio de selección |
actionsVisibility |
'hover' | 'always' |
'hover' |
Cuándo se ven las acciones |
isLoading |
boolean |
false |
Estado de carga |
skeletonCount |
number |
5 |
Filas fantasma |
emptyState |
ReactNode | EmptyStateProps |
— | Estado vacío |
maxHeight |
number | string |
— | Alto máximo con scroll |
staggerAnimation |
boolean |
true |
Entrada escalonada de las filas |
header / footer |
ReactNode |
— | Contenido fijo arriba y abajo |
infinite |
ListInfinite |
— | Carga la página siguiente al llegar al final |
renderItem |
(item: ListItemData, index: number) => ReactNode |
— | Sustituye el renderizado de cada fila |
children |
ReactNode |
— | API de composición: <List.Item /> |
aria-label |
string |
— | Nombre accesible de la lista |
className / itemClassName |
string |
— | Clases |
ListItemData
Sección titulada «ListItemData»| Prop | Tipo | Descripción |
|---|---|---|
id |
string | number |
Clave del elemento |
title |
ReactNode |
Título |
subtitle |
ReactNode |
Segunda línea |
description |
ReactNode |
Texto largo; se recorta a dos líneas |
icon / avatar |
ReactNode |
Bloque izquierdo. Prioridad: avatar > icon > status |
status |
ListItemStatus |
{ tone, label? }: con texto es un chip, sin él un punto |
meta |
ReactNode |
Bloque derecho: importe, fecha, contador |
badge |
ReactNode |
Distintivo |
actions |
ListAction[] |
Botones de icono: { icon, label, onClick, tone?, disabled? } |
content |
ReactNode |
Zona libre bajo el texto: progreso, error, chips |
onClick |
(event: React.MouseEvent) => void |
Click en el elemento |
href |
string |
Ruta interna (<Link>) o URL absoluta (<a>) |
disabled |
boolean |
Desactiva el elemento |
unread |
boolean |
Lo resalta como no leído |
ListSectionData
Sección titulada «ListSectionData»| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
key |
string |
— | Clave de la sección |
items |
ListItemData[] |
— | Elementos |
label |
ReactNode |
— | Encabezado |
icon |
ReactNode |
— | Icono del encabezado |
tone |
ListTone |
'default' |
Color del encabezado |
meta |
ReactNode |
— | Contenido a la derecha del encabezado |
collapsible |
boolean |
false |
Se puede plegar |
defaultOpen |
boolean |
true |
Empieza desplegada |
emptyState |
ReactNode |
— | Qué enseñar si la sección está vacía |
Pagination
Sección titulada «Pagination»El paginador suelto, por si lo necesitas fuera de una Table.
import { Pagination } from '@openfactu/ui';
<Pagination page={pagina} pageSize={tamano} total={totalRegistros} onPageChange={setPagina} onPageSizeChange={setTamano}/>
{/* sin selector de tamaño */}<Pagination page={pagina} pageSize={25} total={480} onPageChange={setPagina} pageSizeOptions={[]} />| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
page |
number |
— | Página actual, 1-based |
pageSize |
number |
— | Registros por página |
total |
number |
— | Total de registros |
onPageChange |
(page: number) => void |
— | Cambio de página |
pageSizeOptions |
number[] |
[10, 25, 50, 100] |
Opciones del selector. [] lo oculta |
onPageSizeChange |
(size: number) => void |
— | Cambio de tamaño |
className |
string |
— | Clases |
FilterBar
Sección titulada «FilterBar»Barra de búsqueda y filtros declarativa: describes los filtros y el componente monta los controles.
import { FilterBar, type FilterBarConfig } from '@openfactu/ui';
const CONFIG: FilterBarConfig[] = [ { key: 'estado', label: 'Estado', type: 'select', options: [ { label: 'Borrador', value: 'borrador' }, { label: 'Emitida', value: 'emitida' }, { label: 'Pagada', value: 'pagada' }, ], }, { key: 'cliente', label: 'Cliente', type: 'search-select', width: 200, options: CLIENTES }, { key: 'desde', label: 'Desde', type: 'date' }, { key: 'referencia', label: 'Referencia', type: 'text' },];
<FilterBar config={CONFIG} searchTerm={termino} onSearchChange={setTermino} activeFilters={filtros} onFilterChange={(k, v) => setFiltros((p) => ({ ...p, [k]: v }))} onClear={() => { setTermino(''); setFiltros({}); }} searchPlaceholder="Buscar facturas…"/>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
config |
FilterBarConfig[] |
— | Filtros a montar |
searchTerm |
string |
— | Texto del buscador |
onSearchChange |
(value: string) => void |
— | Cambio del buscador |
activeFilters |
Record<string, any> |
— | Valores activos, por key |
onFilterChange |
(key: string, value: any) => void |
— | Cambio de un filtro |
onClear |
() => void |
— | Limpiar todo |
searchPlaceholder |
string |
— | Texto del buscador |
shortcut |
'mod+k' | 'ctrl+k' | '/' | false |
'mod+k' |
Atajo que enfoca el buscador |
className |
string |
— | Clases |
FilterBarConfig
Sección titulada «FilterBarConfig»| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
key |
string |
— | Clave del filtro |
label |
string |
— | Etiqueta |
type |
'text' | 'select' | 'search-select' | 'date' |
— | Control a montar |
options |
{ label: string; value: any }[] |
— | Opciones de los desplegables |
placeholder |
string |
— | Texto de marcador |
width |
number |
160 |
Ancho del control en píxeles |
KpiCard
Sección titulada «KpiCard»La tarjeta de cifra del dashboard: etiqueta, valor, subtítulo y tendencia.
import { KpiCard } from '@openfactu/ui';import { Receipt } from 'lucide-react';
<KpiCard label="Facturado este mes" value="48.230,55 €" sub="122 facturas" trend={{ dir: 'up', text: '+12 % vs. mes anterior' }} icon={<Receipt className="h-4 w-4" />}/>
<KpiCard label="Pendiente de cobro" value="12.480,00 €" trend={{ dir: 'down', text: '-4 %' }} /><KpiCard label="Cargando…" value="—" isLoading />isLoading sustituye las cifras por barras de carga sin cambiar la altura, así que
el dashboard no da un salto al terminar de cargar.
| Prop | Tipo | Descripción |
|---|---|---|
label |
string |
Etiqueta |
value |
ReactNode |
Cifra principal |
sub |
ReactNode |
Segunda línea |
trend |
{ dir: 'up' | 'down' | 'flat'; text: string } |
Tendencia con su flecha |
icon |
ReactNode |
Icono decorativo arriba a la derecha |
onClick |
() => void |
Hace la tarjeta clicable |
isLoading |
boolean |
Barras de carga en lugar de las cifras |
className |
string |
Clases |
El contenedor de siempre: título, subtítulo, acción de cabecera, cuerpo y pie.
import { Card, Button } from '@openfactu/ui';
<Card title="Datos fiscales" subtitle="Aparecen en todas las facturas"> <p>…</p></Card>
<Card title="Líneas del documento" headerAction={<Button size="sm" variant="ghost">Añadir</Button>} footer={ <div className="flex justify-end gap-2"> <Button variant="secondary" size="sm">Cancelar</Button> <Button variant="accent" size="sm">Guardar</Button> </div> }> …</Card>noPadding quita el relleno del cuerpo, que es lo que se quiere al meter una Table
dentro para que llegue hasta los bordes.
Estado de carga
Sección titulada «Estado de carga»<Card title="Resumen" isLoading /><Card title="Movimientos" isLoading skeleton="list" /><Card title="Personalizado" isLoading skeleton={<MiEsqueleto />} skeletonHeader={false} />| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
children |
ReactNode |
— | Cuerpo |
title |
ReactNode |
— | Título |
subtitle |
ReactNode |
— | Subtítulo |
headerAction |
ReactNode |
— | Acción a la derecha de la cabecera |
footer |
ReactNode |
— | Pie |
noPadding |
boolean |
false |
Quita el relleno del cuerpo |
isLoading |
boolean |
false |
Sustituye el contenido por un esqueleto |
skeleton |
'text' | 'list' | 'none' | ReactNode |
'text' |
Qué dibujar mientras carga |
skeletonLines |
number |
3 |
Líneas del esqueleto de texto |
skeletonHeader |
boolean |
true |
También esqueletiza título y subtítulo |
className / bodyClassName |
string |
— | Clases |
El distintivo de estado. Extiende React.HTMLAttributes<HTMLDivElement>.
import { Badge } from '@openfactu/ui';
<Badge variant="success">Pagada</Badge><Badge variant="warning">Vence hoy</Badge><Badge variant="danger">Vencida</Badge><Badge variant="info">Contabilizada</Badge><Badge variant="accent">Nueva</Badge><Badge variant="neutral">Borrador</Badge>Variantes: success, warning, danger, error, info, neutral, accent y
teal.
Las variantes de estado no siguen al tema, y es a propósito: un color de estado
tiene que significar lo mismo en todos los temas. La única que sigue al acento del
tenant es accent.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
variant |
BadgeVariant |
'neutral' |
Color del distintivo |
| …resto | HTMLAttributes<HTMLDivElement> |
— | className, onClick, title… |
Iniciales o foto, con indicador de estado opcional.
import { Avatar } from '@openfactu/ui';
<Avatar name="Ana Pérez" /><Avatar name="Ana Pérez" size="xs" /><Avatar name="Bruno Gil" src="/fotos/bruno.jpg" size="lg" /><Avatar name="Carla Ruiz" status="online" />Sin src se pintan las iniciales de las dos primeras palabras del nombre, sobre un
degradado derivado de la escala fija --k-ink-*.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
name |
string |
— | Nombre completo; de ahí salen las iniciales |
src |
string |
— | URL de la foto |
size |
'xs' | 'sm' | 'md' | 'lg' |
'md' |
Tamaño |
status |
'online' | 'offline' | 'busy' | 'away' |
— | Punto de estado |
className |
string |
— | Clases |