Navegación
Cómo se mueve el usuario por la aplicación: la barra de secciones de un módulo, el sidebar, las pestañas, la miga de pan, la cabecera de página y el buscador global.
NavMenu
Sección titulada «NavMenu»La barra de secciones de un módulo. El catálogo le llega plano y los ítems que
comparten group se pliegan solos bajo un desplegable; los que no llevan grupo quedan
sueltos, en el orden en que llegan.
import { NavMenu, type NavMenuItem } from '@openfactu/ui';import { Users, Building2, Wallet } from 'lucide-react';
const SECCIONES: NavMenuItem[] = [ { id: 'empleados', label: 'Empleados', icon: <Users className="h-3.5 w-3.5" /> }, { id: 'departamentos', label: 'Departamentos', icon: <Building2 className="h-3.5 w-3.5" /> },
{ id: 'nominas', label: 'Nóminas', group: 'Nóminas', icon: <Wallet className="h-3.5 w-3.5" /> }, { id: 'conceptos', label: 'Conceptos de nómina', group: 'Nóminas' },
{ id: 'fichajes', label: 'Mis fichajes', group: 'Tiempo y turnos', status: 'beta' }, { id: 'kioskos', label: 'Kioskos de fichaje', group: 'Tiempo y turnos', status: 'beta' },];
<NavMenu items={SECCIONES} value={activa} onChange={setActiva} aria-label="Recursos humanos" />El grupo que contiene la sección activa se queda resaltado aunque esté plegado.
Distintivos de madurez
Sección titulada «Distintivos de madurez»status pinta un distintivo estándar: beta, alpha, nuevo o pronto. Con badge
pones el tuyo y sustituye al de status.
<NavMenu value={activa} onChange={setActiva} items={[ { id: 'estable', label: 'Estable' }, { id: 'beta', label: 'En beta', status: 'beta' }, { id: 'alpha', label: 'En alpha', status: 'alpha' }, { id: 'nuevo', label: 'Recién salido', status: 'nuevo' }, { id: 'pronto', label: 'Próximamente', status: 'pronto', disabled: true }, ]}/>Descripciones y enlaces del router
Sección titulada «Descripciones y enlaces del router»description solo se ve dentro del desplegable de un grupo. renderItem envuelve cada
ítem, para pintar un <Link> del router en lugar de un botón sin perder estilos ni
teclado.
<NavMenu items={SECCIONES} value={activa} onChange={setActiva} renderItem={(item, contenido) => <Link to={`/rrhh/${item.id}`}>{contenido}</Link>}/>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
items |
NavMenuItem[] |
— | Catálogo plano de secciones |
value |
string |
— | id del ítem activo |
onChange |
(id: string, item: NavMenuItem) => void |
— | Cambio de sección |
renderItem |
(item: NavMenuItem, content: ReactNode) => ReactNode |
— | Envoltorio de cada ítem |
size |
'sm' | 'md' |
'md' |
Tamaño |
aria-label |
string |
— | Nombre accesible de la barra |
className |
string |
— | Clases |
NavMenuItem
Sección titulada «NavMenuItem»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
label |
string |
Nombre |
icon |
ReactNode |
Icono |
group |
string |
Grupo bajo el que se pliega |
status |
'beta' | 'alpha' | 'nuevo' | 'pronto' |
Distintivo de madurez |
badge |
ReactNode |
Distintivo propio; sustituye al de status |
description |
string |
Ayuda bajo el nombre, solo dentro del desplegable |
disabled |
boolean |
Desactiva el ítem |
NavItem y NavGroup
Sección titulada «NavItem y NavGroup»Las piezas del sidebar: un enlace y un grupo plegable. Usan los tokens del sidebar
(--sidebar-fg, --sidebar-hover, --sidebar-active), no los del cuerpo de la
aplicación.
import { NavItem, NavGroup } from '@openfactu/ui';import { FileText, Receipt, Wallet } from 'lucide-react';
<NavGroup id="ventas" label="Ventas" icon={Receipt} isOpen={abierto === 'ventas'} onToggle={alternar}> <NavItem label="Facturas" path="/ventas/facturas" icon={FileText} isActive={ruta === '/ventas/facturas'} /> <NavItem label="Cobros" path="/ventas/cobros" icon={Wallet} isActive={ruta === '/ventas/cobros'} /></NavGroup>El icono se pasa como componente, no como elemento: icon={FileText}, no
icon={<FileText />}.
NavItem
Sección titulada «NavItem»| Prop | Tipo | Descripción |
|---|---|---|
label |
string |
Texto del enlace |
path |
string |
Ruta de destino |
icon |
LucideIcon |
Componente de icono |
isActive |
boolean |
Estado activo |
onClick |
() => void |
Click adicional (cerrar el sidebar en móvil) |
className |
string |
Clases |
NavGroup
Sección titulada «NavGroup»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador del grupo |
label |
string |
Título, en versales |
icon |
LucideIcon |
Componente de icono |
isOpen |
boolean |
Desplegado |
onToggle |
(id: string) => void |
Alternar |
children |
ReactNode |
Los NavItem de dentro |
Pestañas que intercambian el contenido de un panel.
import { Tabs } from '@openfactu/ui';
<Tabs value={pestana} onChange={setPestana} items={[ { key: 'general', label: 'General' }, { key: 'lineas', label: 'Líneas', badge: <Badge variant="neutral">12</Badge> }, { key: 'adjuntos', label: 'Adjuntos', icon: <Paperclip className="h-3.5 w-3.5" /> }, { key: 'auditoria', label: 'Auditoría', disabled: true }, ]}/>Variantes
Sección titulada «Variantes»<Tabs variant="underline" … /> {/* subrayado bajo la activa (por defecto) */}<Tabs variant="pill" … /> {/* pastilla translúcida, ligera */}<Tabs variant="segmented" … /> {/* grupo compacto de una pieza */}Con muchas pestañas, scrollable las deja desplazarse en horizontal; fullWidth las
reparte a partes iguales.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
items |
TabItem[] |
— | Pestañas: { key, label, icon?, badge?, disabled? } |
value |
string |
— | key de la pestaña activa |
onChange |
(key: string) => void |
— | Cambio de pestaña |
variant |
'underline' | 'pill' | 'segmented' |
'underline' |
Estilo |
size |
'sm' | 'md' |
'md' |
Tamaño |
scrollable |
boolean |
false |
Desplazamiento horizontal con muchas pestañas |
fullWidth |
boolean |
false |
Cada pestaña ocupa el mismo ancho |
className |
string |
— | Clases |
Breadcrumbs
Sección titulada «Breadcrumbs»La miga de pan.
import { Breadcrumbs } from '@openfactu/ui';
<Breadcrumbs items={[ { label: 'Ventas', href: '/ventas' }, { label: 'Facturas', href: '/ventas/facturas' }, { label: 'FAC/2026/0042' }, ]}/>El último elemento es el actual y no se enlaza. Con href navega con <Link> de
react-router —hace falta un Router en el árbol—; si prefieres controlarlo tú, usa
onClick.
| Prop | Tipo | Descripción |
|---|---|---|
items |
BreadcrumbItem[] |
Elementos: { label, href?, onClick? } |
className |
string |
Clases |
PageHeader
Sección titulada «PageHeader»La cabecera de una página: antetítulo, título, subtítulo, acciones, miga de pan, barra de herramientas y pestañas, todo alineado.
import { PageHeader, Breadcrumbs, Tabs, Button } from '@openfactu/ui';import { Receipt, Plus } from 'lucide-react';
<PageHeader eyebrow="Ventas" title="Facturas" subtitle="122 emitidas este mes" icon={<Receipt className="h-5 w-5" />} breadcrumbs={<Breadcrumbs items={MIGA} />} actions={ <Button variant="accent"> <Plus className="h-3.5 w-3.5" /> Nueva factura </Button> } toolbar={<FilterBar {...filtros} />} tabs={<Tabs value={pestana} onChange={setPestana} items={PESTANAS} />} sticky/>Cada hueco espera lo que su nombre dice: breadcrumbs encima de todo, toolbar bajo
el título y tabs pegadas al borde inferior. La línea separadora sale sola si hay
tabs.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
title |
ReactNode |
— | Título |
subtitle |
ReactNode |
— | Subtítulo |
eyebrow |
ReactNode |
— | Antetítulo en versales pequeñas |
icon |
ReactNode |
— | Icono a la izquierda, en el color de acento |
actions |
ReactNode |
— | Botones y controles a la derecha |
breadcrumbs |
ReactNode |
— | Normalmente un <Breadcrumbs>, encima de todo |
toolbar |
ReactNode |
— | Filtros o buscador bajo el título |
tabs |
ReactNode |
— | Normalmente unas <Tabs>, al borde inferior |
size |
'sm' | 'md' | 'lg' |
'md' |
Tamaño |
divider |
boolean |
true si hay tabs |
Línea separadora inferior |
sticky |
boolean |
false |
Se queda pegado arriba al hacer scroll |
className |
string |
— | Clases |
SearchTrigger
Sección titulada «SearchTrigger»La caja con lupa y el chip del atajo que abre el buscador global. No busca nada: solo abre.
import { SearchTrigger, CommandPalette, useCommandPalette } from '@openfactu/ui';
const paleta = useCommandPalette();
<SearchTrigger onOpen={paleta.openPalette} placeholder="Buscar artículos, clientes…" /><CommandPalette open={paleta.open} onClose={paleta.close} sections={secciones} />| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
onOpen |
() => void |
— | Abre el buscador |
placeholder |
string |
'Buscar…' |
Texto de la caja |
shortcut |
string | false |
'mod+k' |
Atajo que se enseña a la derecha |
size |
'sm' | 'md' |
'md' |
Tamaño |
fullWidth |
boolean |
true |
Ocupa todo el ancho disponible |
aria-label |
string |
— | Nombre accesible |
className |
string |
— | Clases |
CommandPalette
Sección titulada «CommandPalette»El buscador global: resultados agrupados por tipo, acciones ejecutables, atajos, historial y navegación con el teclado.
import { CommandPalette, useCommandPalette, type CommandSection } from '@openfactu/ui';import { Package, Users, FileText, Plus } from 'lucide-react';
const paleta = useCommandPalette();
const secciones: CommandSection[] = [ { key: 'acciones', label: 'Acciones', icon: <Plus className="h-3 w-3" />, items: [ { id: 'nueva-factura', label: 'Nueva factura', icon: <FileText className="h-4 w-4" />, shortcut: ['Ctrl', 'N'], keywords: 'crear emitir venta', onSelect: () => navegar('/ventas/facturas/nueva'), }, ], }, { key: 'articulos', label: 'Artículos', icon: <Package className="h-3 w-3" />, action: { label: 'Ver todos', onSelect: () => navegar('/articulos') }, items: articulos.map((a) => ({ id: a.id, label: a.nombre, meta: a.codigo, keywords: a.codigo, onSelect: () => abrir(a), })), },];
<CommandPalette open={paleta.open} onClose={paleta.close} sections={secciones} recentKey="buscador-global"/>meta es el dato corto alineado a la derecha —el código de un artículo, un importe—;
description es la segunda línea. keywords añade texto por el que también debe
encontrarse el resultado, sin enseñarlo.
action en una sección es el «ver todos» que lleva al listado completo cuando los
resultados son solo una muestra.
Búsqueda en servidor
Sección titulada «Búsqueda en servidor»Con onSearch la paleta deja de filtrar en cliente: le pasas tú las secciones ya
resueltas.
<CommandPalette open={paleta.open} onClose={paleta.close} sections={resultados} onSearch={consultar} debounceMs={200} loading={cargando}/>useCommandPalette()
Sección titulada «useCommandPalette()»Se encarga del estado y del atajo global.
const paleta = useCommandPalette(); // Ctrl/⌘ + Kconst paleta = useCommandPalette({ shortcut: '/' });const paleta = useCommandPalette({ shortcut: false });| Devuelve | Tipo | Descripción |
|---|---|---|
open |
boolean |
Si está abierta |
setOpen |
(open: boolean) => void |
Estado en crudo |
openPalette |
() => void |
Abrir |
close |
() => void |
Cerrar |
Con 'mod+k' el atajo alterna la paleta y funciona con ⌘ en Mac y Ctrl en Windows.
Con '/' solo abre, y se ignora mientras se escribe en un campo.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
open |
boolean |
— | Abierta |
onClose |
() => void |
— | Cerrar |
sections |
CommandSection[] |
— | Resultados ya agrupados |
onSearch |
(term: string) => void |
— | Modo servidor: desactiva el filtrado en cliente |
debounceMs |
number |
200 |
Espera antes de llamar a onSearch |
loading |
boolean |
false |
Indicador de carga |
placeholder |
string |
'Buscar o ejecutar…' |
Texto de la caja |
emptyMessage |
string |
'Sin resultados' |
Texto sin resultados |
recentKey |
string | false |
false |
Clave con la que se guardan las últimas búsquedas |
maxRecent |
number |
5 |
Búsquedas guardadas |
footerHint |
ReactNode | false |
— | Texto de ayuda del pie |
className |
string |
— | Clases |
CommandItem
Sección titulada «CommandItem»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
label |
string |
Texto principal |
description |
string |
Segunda línea: ruta, descripción o contexto |
icon |
ReactNode |
Icono |
meta |
ReactNode |
Dato corto alineado a la derecha |
shortcut |
string[] |
Atajo que se muestra (['Ctrl', 'S']) |
keywords |
string |
Texto extra por el que también se encuentra |
onSelect |
() => void |
Acción al elegirlo |
disabled |
boolean |
Desactiva el resultado |
CommandSection
Sección titulada «CommandSection»| Prop | Tipo | Descripción |
|---|---|---|
key |
string |
Clave de la sección |
label |
string |
Encabezado |
icon |
ReactNode |
Icono del encabezado |
items |
CommandItem[] |
Resultados |
action |
{ label: string; onSelect: () => void } |
El «ver todos» del encabezado |