Ir al contenido

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.

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.

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 },
]}
/>

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
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

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 />}.

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
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 },
]}
/>
<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

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

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

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

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.

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}
/>

Se encarga del estado y del atajo global.

const paleta = useCommandPalette(); // Ctrl/⌘ + K
const 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
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
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