Navegación (context bar + tabs + sidebar)
La estructura de toda la app. Tabs para secciones, sidebar solo dentro de Ajustes.
Así está armado el panel: barra de contexto (wordmark, selector de organización, búsqueda, QR, notificaciones, reportar un problema y cuenta), tabs de sección con ícono y navegación lateral agrupada dentro de Ajustes. Todo en el azul de Moodinary; el color de la organización solo aparece en el formulario público.
Instalación
shadcn
npx shadcn@latest add https://design.moodinary.com/r/app-shell.jsonimport
import { AppShell, AppHeader, AppSheet, ContextBar, ContextSep, contextButtonClass, contextIconClass, NavTabs, navTabClass, SideLayout, SideNav, SideNavGroup, sideNavLinkClass } from "@moodinary/ui/components/app-shell";Ejemplos
Panel completo
Las secciones son links con navTabClass dentro de NavTabs (no el componente Tabs). Ajustes abre SideNav con tres grupos: Organización, Desarrolladores y Registro. Marcá la página actual con aria-current="page".
Código
structure.tsx
import {
AppHeader,
AppSheet,
AppShell,
ContextBar,
ContextSep,
contextButtonClass,
contextIconClass,
NavTabs,
navTabClass,
SideLayout,
SideNav,
SideNavGroup,
sideNavLinkClass,
} from "@moodinary/ui/components/app-shell";
import { Button } from "@moodinary/ui/components/button";
import { CommandTrigger } from "@moodinary/ui/components/command";
import {
BalanceScaleIcon,
BellIcon,
Blocks,
BugIcon,
CalendarDays,
CheckCircle,
CodeIcon,
CreditCard,
HeartPulseIcon,
House,
type IconComponent,
PaintBoardIcon,
QrCodeIcon,
ScrollText,
Settings,
TrendingUp,
TriangleAlertIcon,
Users,
Wrench,
} from "@moodinary/ui/components/icons";
import { BRAND_ASSETS } from "@moodinary/ui/lib/brand-assets";
import { cn } from "@moodinary/ui/lib/utils";
import { useState } from "react";
const tabs: [string, string, IconComponent][] = [
["inicio", "Inicio", House],
["pulsos", "Pulsos", HeartPulseIcon],
["acciones", "Acciones", CheckCircle],
["analiticas", "Analíticas", TrendingUp],
["calendario", "Calendario", CalendarDays],
["alertas", "Alertas", TriangleAlertIcon],
["ajustes", "Ajustes", Settings],
];
const settings: [string, [string, string, IconComponent][]][] = [
[
"Organización",
[
["equipos", "Equipos", Users],
["personalizacion", "Personalización", PaintBoardIcon],
["facturacion", "Facturación", CreditCard],
["comparativa", "Comparativa", BalanceScaleIcon],
["configuracion", "Configuración", Wrench],
],
],
[
"Desarrolladores",
[
["api", "API", CodeIcon],
["conectores", "Conectores", Blocks],
],
],
["Registro", [["auditoria", "Auditoría", ScrollText]]],
];
export default function NavigationStructure() {
const [tab, setTab] = useState("ajustes");
const [page, setPage] = useState("equipos");
const [open, setOpen] = useState(false);
const label = settings
.flatMap(([, items]) => items)
.find(([seg]) => seg === page)?.[1];
return (
<div className="w-full">
<p className="mb-3 text-ink-3 text-xs">
Organización y datos ilustrativos.
</p>
<AppShell className="min-h-0 overflow-hidden rounded-[10px] border border-line">
<AppHeader className="static">
<ContextBar>
<img
alt="Moodinary"
className="block h-5 w-auto"
height={20}
src={BRAND_ASSETS.wordmark}
width={71}
/>
<ContextSep />
<button
aria-label="Organización: Organización de ejemplo. Cambiar"
className={contextButtonClass}
type="button"
>
<span className="grid size-6 shrink-0 place-items-center rounded-md bg-primary font-semibold text-[11px] text-primary-foreground">
OE
</span>
<span className="max-w-[45vw] truncate font-medium md:max-w-[40vw]">
Organización de ejemplo
</span>
<svg
aria-hidden
className="shrink-0 text-ink-3"
fill="none"
height="14"
stroke="currentColor"
strokeWidth="1.5"
viewBox="0 0 16 16"
width="14"
>
<path d="M5 6l3-3 3 3M5 10l3 3 3-3" />
</svg>
</button>
<span className="flex-1" />
<CommandTrigger className="mr-1.5" />
<Button
className="mr-1.5 max-md:hidden"
size="sm"
variant="secondary"
>
<QrCodeIcon aria-hidden className="size-4" />
Generar QR
</Button>
<button
aria-label="Generar QR"
className={cn(contextIconClass, "md:hidden")}
type="button"
>
<QrCodeIcon aria-hidden className="size-4" />
</button>
<a
aria-label="Notificaciones"
className={contextIconClass}
href="#notificaciones"
>
<BellIcon aria-hidden className="size-4" />
</a>
<button
aria-label="Reportar un problema"
className={contextIconClass}
title="Reportar un problema"
type="button"
>
<BugIcon aria-hidden className="size-4" />
</button>
<button
aria-label="Cuenta"
className="grid size-7 shrink-0 place-items-center rounded-full bg-primary font-semibold text-[11px] text-primary-foreground"
type="button"
>
MN
</button>
</ContextBar>
<NavTabs aria-label="Secciones">
{tabs.map(([seg, name, Icon]) => (
<a
aria-current={tab === seg ? "page" : undefined}
className={navTabClass(tab === seg)}
href={`#${seg}`}
key={seg}
onClick={(event) => {
event.preventDefault();
setTab(seg);
}}
>
<Icon aria-hidden />
{name}
</a>
))}
</NavTabs>
</AppHeader>
<AppSheet>
{tab === "ajustes" ? (
<SideLayout className="p-4">
<SideNav aria-label="Ajustes" className="static">
{settings.map(([group, items]) => (
<SideNavGroup key={group} label={group}>
{items.map(([seg, name, Icon]) => (
<a
aria-current={page === seg ? "page" : undefined}
className={sideNavLinkClass(page === seg)}
href={`#${seg}`}
key={seg}
onClick={(event) => {
event.preventDefault();
setPage(seg);
setOpen(false);
}}
>
<Icon aria-hidden />
{name}
</a>
))}
</SideNavGroup>
))}
</SideNav>
<section aria-label={label} className="min-w-0">
<button
aria-expanded={open}
className="mb-3 flex h-9 w-max items-center gap-2 rounded-lg border border-line bg-white px-2.5 font-medium shadow-card md:hidden"
onClick={() => setOpen((o) => !o)}
type="button"
>
<svg
aria-hidden
fill="none"
height="14"
stroke="currentColor"
strokeWidth="1.6"
viewBox="0 0 16 16"
width="14"
>
<path d="M2.5 4h11M2.5 8h11M2.5 12h11" />
</svg>
Ajustes · {label}
<svg
aria-hidden
className="text-ink-3"
fill="none"
height="12"
stroke="currentColor"
strokeWidth="1.6"
viewBox="0 0 16 16"
width="12"
>
<path d="M4 6l4 4 4-4" />
</svg>
</button>
{open ? (
<nav
aria-label="Ajustes"
className="mb-4 rounded-[10px] border border-line bg-white p-1.5 shadow-pop md:hidden [&>div+div]:pt-2"
>
{settings.map(([group, items]) => (
<SideNavGroup key={group} label={group}>
{items.map(([seg, name, Icon]) => (
<a
aria-current={page === seg ? "page" : undefined}
className={sideNavLinkClass(page === seg)}
href={`#${seg}`}
key={seg}
onClick={(event) => {
event.preventDefault();
setPage(seg);
setOpen(false);
}}
>
<Icon aria-hidden />
{name}
</a>
))}
</SideNavGroup>
))}
</nav>
) : null}
<h3 className="font-semibold text-lg">{label}</h3>
<p className="mt-1 text-ink-2">
Ajustes de la organización de ejemplo.
</p>
</section>
</SideLayout>
) : (
<div className="p-5 text-ink-2">
{tabs.find(([seg]) => seg === tab)?.[1]} de la organización de
ejemplo.
</div>
)}
</AppSheet>
</AppShell>
</div>
);
}Props
SideNavGroup
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| label | ReactNode | - | Título del grupo. |
| children | ReactNode | - | Links del grupo. |
sideNavLinkClass
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| active | boolean | - | Devuelve las clases del link activo o inactivo. |
navTabClass
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| active | boolean | - | Clases del link de sección; activo suma subrayado y texto en tinta. |
contextButtonClass, contextIconClass
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| (string) | string | - | Clases para el selector de organización y para los íconos cuadrados de la barra (campana, reportar). |
AppShell, AppHeader, AppSheet, ContextBar, ContextSep, NavTabs, SideLayout, SideNav
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| ...props | ComponentProps del elemento | - | Contenedores con estilos de layout. |
| className | string | - | Clases extra. |
Cuándo sí y cuándo no
Hacé
- Mostrá la organización actual en la barra de contexto.
- Usá SideNav solo para las subpáginas de Ajustes.
- Mantené el orden de las secciones en todo el producto.
Evitá
- No uses el componente Tabs para las secciones: son links.
- No pongas el plan en el encabezado de página.
- No uses SideNav para acciones.
Accesibilidad
- NavTabs y SideNav son
navconaria-label(Secciones, Ajustes). - Los botones de ícono llevan
aria-label: Notificaciones, Reportar un problema, Cuenta. - El link actual lleva
aria-current="page".