componente · @solu30/ui-kit/navigation
ContextualSidebar
Barra lateral de secciones en acordeón, con panel contextual de segundo nivel y modo colapsado.
Seguir leyendo — 3 párrafos más
ContextualSidebar — domain-agnostic collapsible sidebar with L1 accordion sections and optional L2 contextual drilldown panel.
ADR-027: aprobado por Martín (2026-06-13), integrado al barrel público (@solu30/ui-kit/navigation) y publicado en 0.77.0.
ADR-027 addendum (2026-06-20): prop tone?: 'default' | 'comingSoon' en ContextualSidebarItem y ContextualSidebarSection. Aditivo, non-breaking. Coloración ámbar (orange-600/orange-500/orange-50) para bloques "Próximamente" — distinta del gris neutro, sin opacity supresiva.
1 sitio de import en 1 de 15 verticales — un archivo que la importa cuenta una vez, monte una instancia o veinte.
- luigi
La pieza, con lugar de pantalla
Cargando el espécimen…
export function ContextualSidebarMatrix() {
const [activeId, setActiveId] = useState('mi-franquicia/pedidos')
const [collapsed, setCollapsed] = useState(false)
const sections: ContextualSidebarSection[] = [
{
id: 'novedades',
label: 'Novedades',
collapsible: false,
icon: <IconSparkles className="h-4 w-4" />,
content: [
{ type: 'item', item: { id: 'novedades', label: 'Novedades', icon: <IconSparkles className="h-4 w-4" /> } },
],
},
{
id: 'mi-franquicia',
label: 'Mi franquicia',
icon: <IconHome className="h-4 w-4" />,
decorativeAccent: 'accent-1',
content: [
{ type: 'item', item: { id: 'mi-franquicia/tablero', label: 'Tablero', icon: <IconGrid className="h-4 w-4" /> } },
{
type: 'item',
item: {
id: 'mi-franquicia/pedidos',
label: 'Pedidos',
icon: <IconInbox className="h-4 w-4" />,
badge: 3,
badgeTone: 'warning',
},
},
{
type: 'item',
item: {
id: 'mi-franquicia/reclamos',
label: 'Reclamos',
icon: <IconBell className="h-4 w-4" />,
badge: 1,
badgeTone: 'error',
},
},
],
},
{
// Sin `decorativeAccent`: «Productos usa primary (marca, sin slot)»
// — comentario textual de `module-menu-comercial.ts:26`.
id: 'productos',
label: 'Productos',
icon: <IconGrid className="h-4 w-4" />,
items: [
{ id: 'productos/catalogo', label: 'Catálogo', icon: <IconGrid className="h-4 w-4" /> },
{ id: 'productos/precios', label: 'Precios', icon: <IconChart className="h-4 w-4" /> },
],
},
{
id: 'rrhh',
label: 'RRHH',
icon: <IconTeam className="h-4 w-4" />,
decorativeAccent: 'accent-5',
items: [
{ id: 'rrhh/personas', label: 'Personas', icon: <IconTeam className="h-4 w-4" /> },
{ id: 'rrhh/puestos', label: 'Puestos', icon: <IconClipboard className="h-4 w-4" /> },
{ id: 'rrhh/organigrama', label: 'Organigrama', icon: <IconNetwork className="h-4 w-4" /> },
],
},
{
// Comparte `accent-5` con RRHH — decisión de Martín, 2026-09-01.
id: 'academia',
label: 'Academia',
icon: <IconBook className="h-4 w-4" />,
decorativeAccent: 'accent-5',
items: [
{ id: 'academia/cursos', label: 'Cursos', icon: <IconBook className="h-4 w-4" /> },
{ id: 'academia/conocimiento', label: 'Sobre Luigi', icon: <IconLightbulb className="h-4 w-4" /> },
],
},
{
id: 'red',
label: 'Locales',
icon: <IconNetwork className="h-4 w-4" />,
decorativeAccent: 'accent-6',
items: [
{ id: 'red/franquicias', label: 'Locales', icon: <IconHome className="h-4 w-4" /> },
{ id: 'red/pedidos', label: 'Pedidos', icon: <IconInbox className="h-4 w-4" />, badge: 7, badgeTone: 'warning' },
],
},
{
// Comparte `accent-6` con Locales — el techo de 6 no se mueve.
id: 'finanzas',
label: 'Finanzas',
icon: <IconChart className="h-4 w-4" />,
decorativeAccent: 'accent-6',
items: [
{ id: 'finanzas/cobros', label: 'Cobros', icon: <IconChart className="h-4 w-4" /> },
{ id: 'finanzas/facturacion', label: 'Facturación', icon: <IconClipboard className="h-4 w-4" /> },
],
},
]
return (
<Stage label="ContextualSidebar · el menú del admin de Luigi — badges que suben al acordeón cerrado, acentos repetidos, footer con el menú de cuenta" tall wide>
<div className="flex flex-col items-center gap-2 px-1 py-1">
<BrowserFrame productName="luigi-admin" className="w-[340px]">
<div className="h-[460px] overflow-hidden">
<ContextualSidebar
sections={sections}
activeItemId={activeId}
onNavigate={(item) => setActiveId(item.id)}
decorativeGlow={false}
sectionBadgeRollup
header={
<div className="flex w-full items-center justify-between gap-2">
<span className="text-sm font-semibold text-content">Luigi</span>
<IconBell className="h-4 w-4 text-content/50" />
</div>
}
headerHeightClass="h-[62px]"
backTo={{ label: 'Volver al hub', onBack: () => {} }}
footer={
<SidebarUserMenu
user={{ name: 'Rocío Peralta', email: 'rocio@luigi.com.ar' }}
collapsed={collapsed}
items={[
{ id: 'perfil', icon: <IconUser className="h-4 w-4" />, label: 'Perfil' },
{ id: 'seguridad', icon: <IconShield className="h-4 w-4" />, label: 'Seguridad' },
]}
onLogout={() => {}}
themeToggle={<ThemeToggle variant="full" />}
/>
}
collapsed={collapsed}
onCollapsedChange={setCollapsed}
className="h-full"
/>
</div>
</BrowserFrame>
<p className="max-w-[440px] text-center text-[11px] text-content/60">
Con el acordeón cerrado, «Mi franquicia» y «Locales» muestran la suma de los badges de
sus hojas; al abrirlos desaparece, porque abajo está el del hijo. Eso es
`sectionBadgeRollup`, y no cuesta ninguna consulta extra.
</p>
</div>
</Stage>
)
}De specimens/navigation.tsx · ContextualSidebarMatrix: el espécimen del Muestrario, no un ejemplo oficial del paquete. Muestra cómo se usa la pieza de verdad, montada contra los tipos instalados —eso es lo que vale— pero no es documentación del kit.
Cómo se usa
import { ContextualSidebar } from '@solu30/ui-kit/navigation'Props
| Prop | Requerida | Tipo | Qué hace |
|---|---|---|---|
| sections | sí | ContextualSidebarSection[] | L1 sections with their items |
| activeItemId | opcional | string | ID of the currently active L1 item — auto-opens the containing section |
| activeSubItems | opcional | ContextualSidebarItem[] | When provided (non-empty), replaces the L1 view with an L2 sub-panel. The consumer controls which items to show. |
| subPanelLabel | opcional | string | Header label shown at the top of the L2 sub-panel |
| onSubPanelBack | opcional | () => void | Called when the user clicks "back" from the L2 sub-panel |
| onNavigate | sí | (item: ContextualSidebarItem) => void | Navigation callback — called with the clicked item; consumer does the push |
| resolveIcon | opcional | (name: string) => ReactNode | Maps string icon names to ReactNodes. Used when `item.icon` is a string (e.g. "IconClipboard"). Falls back to null when not provided or name not found. |
| collapsed | opcional | boolean | Controlled collapsed (icon-only) state |
| onCollapsedChange | opcional | (collapsed: boolean) => void | Called when the collapse toggle is clicked |
| header | opcional | ReactNode | { title: string; subtitle?: string; } | Sidebar header. Pass a ReactNode for full control, or `{ title, subtitle? }` for default layout. |
| headerHeightClass | opcional | string | Overrides the header row height (default `h-14` = 56px). Pass a Tailwind height class (e.g. `"h-16"`) so the sidebar header can be aligned with the consumer's content header band. |
| backTo | opcional | ContextualSidebarBackTo | Optional "back" affordance rendered at the top of the sidebar body (below the header, above the sections). Shown only within internal/nested sections — the consumer passes it when it applies. Collapses to icon-only when the rail is collapsed. |
| footer | opcional | ReactNode | Footer slot (user menu, settings link, etc.) |
| className | opcional | string | Extra class names on the root aside element |
| subPanelAlwaysExpanded | opcional | boolean | When true, the L2 sub-panel renders readable labels even if the rail is collapsed (items get isCollapsed=false, back-button shows its label). ADR-077. |
| collapsedMode | opcional | ContextualSidebarCollapsedMode | Collapsed-rail mode. Default 'items' (current behavior). ADR-077. |
| onSectionActivate | opcional | (section: ContextualSidebarSection) => void | Called when a section icon is clicked in collapsedMode="sections". Consumer drives the drill-in (e.g. set activeSubItems to the section items). ADR-077. |
| openState | opcional | ContextualSidebarOpenState | Controlled open/closed state for enhanced sections/groups (`content`). Uncontrolled when omitted — internal state seeded from `defaultOpenState`. ADR-027 addendum v2. |
| defaultOpenState | opcional | ContextualSidebarOpenState | Initial open/closed state when uncontrolled. ADR-027 addendum v2. |
| onOpenStateChange | opcional | (next: ContextualSidebarOpenState) => void | Called on every open/closed change (controlled mode: the ONLY way state changes — the component never mutates internally). ADR-027 addendum v2. |
| decorativeGlow | opcional | boolean | When `false`, removes the `drop-shadow-[0_0_6px_currentColor]` glow from the accented icons (active leaf item, section header with an active child) — the accent color itself is untouched, only the halo. Default `true` = current behavior, byte-identical for any consumer that omits this prop. ADR-027 addendum v5. |
| sectionBadgeRollup | opcional | boolean | When `true`, a CLOSED collapsible section shows, on its own header, the SUM of the numeric badges of the items it hides — with the most severe tone among them (`error` > `warning` > `info` > `accent` > default > `success`). Same treatment on the collapsed rail in `collapsedMode="sections"`, where the bubble sits on the section icon. Without this, a counter placed on a leaf only exists once the user opens the accordion that hides it — i.e. it never warns the person who is not already looking there. Text badges (`badge: "New"`) never participate: a roll-up is a count. Default `false` = current behavior, byte-identical for any consumer that omits this prop. ADR-027 addendum v6. |
Ejemplo del paquete
<ContextualSidebar
sections={LUIGI_SECTIONS}
activeItemId={activeId}
onNavigate={(item) => router.push(item.href ?? "/")}
header={{ title: "Luigi" }}
/>Del @example del JSDoc de @solu30/ui-kit, sin editar: son 65 de los 541 símbolos. La sangría y los renglones en blanco son los del paquete.