Botones y acciones
Los cinco componentes con los que el usuario dispara acciones: el botón suelto, los dos menús, el selector de opción única y la barra de acciones masivas.
El botón de toda la vida. Extiende React.ButtonHTMLAttributes<HTMLButtonElement>, así
que onClick, type, form, aria-* y demás pasan directos al <button>.
import { Button } from '@openfactu/ui';
<Button onClick={guardar}>Guardar</Button>Variantes
Sección titulada «Variantes»Seis, y ninguna usa variantes dark:: los tokens ya cambian de valor según el modo,
de forma que el botón sigue al tema del tenant sea cual sea.
<Button variant="primary">Primary</Button><Button variant="accent">Accent</Button><Button variant="secondary">Secondary</Button><Button variant="danger">Danger</Button><Button variant="ghost">Ghost</Button><Button variant="outline">Outline</Button>Tamaños y estados
Sección titulada «Tamaños y estados»<Button size="sm">Small</Button><Button size="md">Medium</Button><Button size="lg">Large</Button>
<Button isLoading>Guardando…</Button><Button disabled>Deshabilitado</Button>isLoading pinta un spinner delante del texto y deshabilita el botón, así que no
hace falta combinarlo con disabled para evitar el doble envío.
Con icono
Sección titulada «Con icono»No hay prop icon: el icono va como hijo. El botón ya es un inline-flex con
gap-1.5, así que se alinea solo.
import { Plus, Trash2 } from 'lucide-react';
<Button> <Plus className="h-3.5 w-3.5" /> Nueva factura</Button>
<Button variant="danger"> <Trash2 className="h-3.5 w-3.5" /> Eliminar</Button>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
variant |
'primary' | 'accent' | 'secondary' | 'danger' | 'ghost' | 'outline' |
'primary' |
Estilo del botón |
size |
'sm' | 'md' | 'lg' |
'md' |
Tamaño |
isLoading |
boolean |
— | Muestra un spinner y deshabilita el botón |
| …resto | ButtonHTMLAttributes |
— | onClick, type, disabled, className… |
DropdownMenu
Sección titulada «DropdownMenu»Menú desplegable anclado a un disparador. El disparador es cualquier nodo que le pases
como hijo; el menú se renderiza en un portal a document.body, así que no lo recorta
ningún overflow.
import { DropdownMenu, Button } from '@openfactu/ui';import { Pencil, Copy, Download, Trash2 } from 'lucide-react';
const acciones = [ { label: 'Editar', icon: <Pencil className="h-3.5 w-3.5" />, onClick: editar }, { label: 'Duplicar', icon: <Copy className="h-3.5 w-3.5" />, onClick: duplicar }, { label: 'Exportar PDF', icon: <Download className="h-3.5 w-3.5" />, disabled: true }, { label: 'Eliminar', icon: <Trash2 className="h-3.5 w-3.5" />, destructive: true, separatorBefore: true, onClick: eliminar, },];
<DropdownMenu items={acciones}> <Button variant="secondary">Acciones</Button></DropdownMenu>destructive pinta el ítem en rojo y separatorBefore le mete una línea encima, que
es como se separa «Eliminar» del resto sin declarar separadores sueltos.
Disparador de icono y alineación
Sección titulada «Disparador de icono y alineación»import { MoreHorizontal } from 'lucide-react';
<DropdownMenu items={acciones}> <button type="button" className="p-1.5 rounded-[2px] text-[var(--fg-muted)] hover:text-accent transition-colors" > <MoreHorizontal className="h-4 w-4" /> </button></DropdownMenu>
<DropdownMenu items={acciones} align="end"> <Button variant="secondary">Alineado a la derecha</Button></DropdownMenu>Teclado
Sección titulada «Teclado»El disparador es tabulable. Con el menú cerrado, Enter, Espacio o ↓ lo abren;
abierto, ↑/↓ recorren los ítems saltándose los deshabilitados, Enter o Espacio
activan el que esté marcado y Escape cierra. También se cierra al hacer scroll.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
items |
DropdownMenuItem[] |
— | Ítems del menú |
children |
ReactNode |
— | Disparador; se envuelve en un span clicable |
align |
'start' | 'end' |
'start' |
Alineación del menú respecto al disparador |
disabled |
boolean |
false |
Desactiva el disparador |
className |
string |
— | Clases del disparador |
menuClassName |
string |
— | Clases del panel del menú |
El menú tiene un ancho mínimo de 180 px.
Forma de un ítem
Sección titulada «Forma de un ítem»DropdownMenuItem es el shape compartido por los tres menús de la librería: el
desplegable, el contextual y las rowActions de Table.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
label |
string |
— | Texto del ítem |
icon |
ReactNode |
— | Icono a la izquierda |
onClick |
() => void |
— | Acción al activarlo |
destructive |
boolean |
— | Lo pinta en rojo (--k-danger-fg) |
disabled |
boolean |
— | Lo desactiva y lo salta al navegar con el teclado |
separatorBefore |
boolean |
— | Dibuja una línea encima del ítem |
ContextMenu
Sección titulada «ContextMenu»El menú del click derecho. Se posiciona en las coordenadas del cursor, se recorta
contra el borde de la ventana para no salirse, y se cierra con click fuera, scroll,
resize o Escape.
Casi siempre se usa a través de useContextMenu, que se encarga del estado:
import { useContextMenu } from '@openfactu/ui';import { Pencil, Copy, FolderInput, Trash2 } from 'lucide-react';
function ListaDeFacturas() { const { contextMenu, openContextMenu } = useContextMenu();
return ( <> <div onContextMenu={(e) => openContextMenu(e, [ { label: 'Editar', icon: <Pencil className="h-3.5 w-3.5" /> }, { label: 'Duplicar', icon: <Copy className="h-3.5 w-3.5" /> }, { label: 'Mover a…', icon: <FolderInput className="h-3.5 w-3.5" />, submenu: [ { label: 'Borradores' }, { label: 'Archivadas' }, { label: 'Papelera', destructive: true }, ], }, { label: 'Eliminar', icon: <Trash2 className="h-3.5 w-3.5" />, destructive: true, separatorBefore: true, }, ]) } > Click derecho aquí </div> {contextMenu} </> );}Submenús
Sección titulada «Submenús»Un ítem con submenu abre un segundo panel al pasar el ratón. Solo un nivel: si
declaras submenu, se ignora su onClick.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
x |
number |
— | Coordenada horizontal del cursor |
y |
number |
— | Coordenada vertical del cursor |
items |
ContextMenuItem[] |
— | Ítems del menú |
onClose |
() => void |
— | Se llama al cerrar |
ContextMenuItem es un ítem de menú más submenu?: ContextMenuItem[].
El menú mide 220 px de ancho.
useContextMenu()
Sección titulada «useContextMenu()»| Devuelve | Tipo | Descripción |
|---|---|---|
contextMenu |
ReactNode |
Renderizar siempre; es null con el menú cerrado |
openContextMenu |
(event: React.MouseEvent, items: ContextMenuItem[]) => void |
Pasar a onContextMenu; hace preventDefault y abre en el cursor |
closeContextMenu |
() => void |
Cierra el menú a mano |
SegmentedControl
Sección titulada «SegmentedControl»Selector de opción única con las opciones pegadas en una sola pieza. Sirve para filtrar o parametrizar lo que se está viendo: el periodo de un panel, la escala de un calendario, la capa de un mapa.
import { SegmentedControl } from '@openfactu/ui';
const [ambito, setAmbito] = React.useState<'active' | 'all'>('active');
<SegmentedControl size="sm" uppercase aria-label="Ámbito del periodo" value={ambito} onChange={setAmbito} options={[ { value: 'active', label: 'Periodo activo' }, { value: 'all', label: 'Histórico' }, ]}/>Variantes
Sección titulada «Variantes»{/* solid — la opción activa se rellena con el acento */}<SegmentedControl aria-label="Escala del calendario" value={escala} onChange={setEscala} options={[ { value: 'week', label: 'Semana' }, { value: 'month', label: 'Mes' }, { value: 'quarter', label: 'Trimestre', disabled: true }, ]}/>
{/* raised — pastilla elevada sobre una pista, sin color de marca */}<SegmentedControl variant="raised" aria-label="Modo del editor" value={modo} onChange={setModo} options={[ { value: 'visual', label: 'Visual', icon: <LayoutGrid className="h-3.5 w-3.5" /> }, { value: 'code', label: 'Avanzado', icon: <ListIcon className="h-3.5 w-3.5" /> }, ]}/>Ocupando todo el ancho
Sección titulada «Ocupando todo el ancho»<SegmentedControl fullWidth aria-label="Vista" value={vista} onChange={setVista} options={[ { value: 'list', label: 'Lista', icon: <ListIcon className="h-3.5 w-3.5" /> }, { value: 'grid', label: 'Tarjetas', icon: <LayoutGrid className="h-3.5 w-3.5" /> }, { value: 'cal', label: 'Calendario', icon: <Calendar className="h-3.5 w-3.5" /> }, ]}/>Teclado
Sección titulada «Teclado»Hay un solo punto de tabulación en todo el grupo, como manda el patrón de grupo de
radio: dentro se navega con ←/→/↑/↓, que además cambian el valor, y con
Inicio/Fin para ir a los extremos.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
options |
SegmentedOption<T>[] |
— | Opciones |
value |
T |
— | Opción activa |
onChange |
(value: T) => void |
— | Cambio de opción |
size |
'sm' | 'md' |
'md' |
Tamaño |
variant |
'solid' | 'raised' |
'solid' |
solid rellena con el acento; raised es una pastilla elevada |
uppercase |
boolean |
false |
Versales con tracking, para barras compactas |
fullWidth |
boolean |
false |
Reparte las opciones por todo el ancho |
disabled |
boolean |
false |
Desactiva el grupo entero |
aria-label |
string |
— | Nombre accesible del grupo |
className |
string |
— | Clases del contenedor |
SegmentedOption<T>
Sección titulada «SegmentedOption<T>»| Prop | Tipo | Descripción |
|---|---|---|
value |
T |
Valor de la opción |
label |
ReactNode |
Etiqueta |
icon |
ReactNode |
Icono a la izquierda |
disabled |
boolean |
Desactiva solo esa opción |
title |
string |
Nombre accesible cuando la opción es solo un icono |
BulkActionsBar
Sección titulada «BulkActionsBar»La barra que aparece encima de una Table cuando hay filas
seleccionadas: el conteo, un enlace para deseleccionar y un hueco a la derecha para
las acciones.
import { BulkActionsBar, Button } from '@openfactu/ui';
<BulkActionsBar count={seleccionadas.size} onClear={() => setSeleccionadas(new Set())}> <Button variant="secondary" onClick={exportar}>Exportar</Button> <Button variant="danger" onClick={eliminar}>Eliminar</Button></BulkActionsBar>Es sticky arriba, de modo que sigue visible mientras se recorre una tabla larga.
Cambiar el texto
Sección titulada «Cambiar el texto»<BulkActionsBar count={3} label="líneas seleccionadas" onClear={limpiar}> <Button onClick={agrupar}>Agrupar en un albarán</Button></BulkActionsBar>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
count |
number |
— | Filas seleccionadas. Con 0 no renderiza nada |
onClear |
() => void |
— | Limpia la selección. Sin él no se pinta el enlace «Deseleccionar» |
children |
ReactNode |
— | Acciones, alineadas a la derecha |
label |
string |
'seleccionados' |
Texto tras el número |
className |
string |
— | Clases del contenedor |