Pollito Blog
October 20, 2025

Proyectos de Software Grandes: Navegación Moderna

Posted on October 20, 2025  •  6 minutes  • 1149 words  • Other languages:  English

Este post es parte de mi serie de blogs sobre Proyectos de Software Grandes .

Después de establecer nuestra estructura de proyecto y sistema de ruteo, es hora de armar una base de UI como la gente. Hoy vamos a implementar un layout de sidebar profesional, le meteremos soporte para dark mode, y crearemos un sistema de navegación que se pueda mantener fácilmente, basado en nuestra configuración de rutas centralizada.

Código Fuente

Todos los snippets de código que aparecen en este post están disponibles en la rama dedicada a este artículo en el repo de GitHub del proyecto:

https://github.com/franBec/tas/tree/feature/2025-10-20

Buscando un Layout

Al construir una aplicación grande, usar componentes estándar te acelera el desarrollo un montón. Yo quería un sidebar compacto y colapsable, algo muy común en aplicaciones tipo dashboard.

Una vez más, navegué los blocks que da shadcn/ui y me quedé con el Sidebar 08 por el equilibrio entre utilidad y estética moderna.

sidebar 08

Para traer los archivos necesarios, ejecuté lo siguiente:

pnpm dlx shadcn@latest add sidebar-08 --overwrite

Este comando crea varios archivos, pero nos vamos en app-sidebar.tsx - el componente principal del sidebar.

Si iniciás el servidor de desarrollo ahora y visitás /dashboard, vas a ver el layout básico en acción.

Personalizando el Sidebar

El sidebar que se generó viene con contenido de relleno (placeholder), así que vamos a dejarlo a nuestro gusto.

sidebar content type

Reemplazando el Header del Sidebar

En app-sidebar.tsx, el componente SidebarMenuButton actúa como el logo/enlace principal.

Reemplacemos la marca genérica con algo que encaje con nuestro proyecto. Quería un ícono de gobierno/organización, así que navegué REMIX ICON y encontré government-fill , que queda perfecto.

Tip pro: Asegurate que el logo sea clickeable y redirija a “/”, eso es un comportamiento UX estándar que los usuarios esperan.

<Sidebar variant="inset" {...props}>
    <SidebarHeader>
        <SidebarMenu>
            <SidebarMenuItem>
                <SidebarMenuButton size="lg" asChild>
                    <Link href="/">
                        <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
                            <img
                                src="/government-fill.svg"
                                alt="Government"
                                className="size-4 invert"
                            />
                        </div>
                        <div className="grid flex-1 text-left text-sm leading-tight">
                  <span className="truncate font-medium">
                    Municipal Services
                  </span>
                            <span className="truncate text-xs">San Luis</span>
                        </div>
                    </Link>
                </SidebarMenuButton>
            </SidebarMenuItem>
        </SidebarMenu>
    </SidebarHeader>
    //rest of the Sidebar component
</Sidebar>

SidebarHeader

Pequeña Misión Secundaria: Favicon

Ya que tenía el ícono listo, me tomé un momento para convertir el SVG government-fill a un archivo .ico y lo coloqué en public/favicon.ico. Es un detalle menor, pero clave para la experiencia del usuario y la consistencia de la marca.

El sidebar por defecto de shadcn usa datos de array hardcodeados para sus enlaces. Para un proyecto grande, la navegación tiene que salir de nuestra fuente central de la verdad: src/lib/routes.ts.

Primero, extendemos la interfaz RouteNode:

// src/lib/routes.ts

export enum SidebarContentType {
    NAV_MAIN_ROOT = "NAV_MAIN_ROOT",
    NAV_MAIN_ITEM = "NAV_MAIN_ITEM",
    NAV_SECONDARY_ITEM = "NAV_SECONDARY_ITEM",
}

export interface RouteNode {
    uri: string;
    title?: string;
    subtitle?: string;
    icon?: React.ComponentType<{ className?: string }>;
    children?: Record<string, RouteNode>;
    sidebarContent?: SidebarContentType;
}

Lógica de Renderizado del Sidebar

Actualizamos app-sidebar.tsx para que itere sobre las rutas y las renderice por sección:

  1. Navegación Principal (nav-main.tsx): Renderiza las áreas de aplicación de alto nivel (por ejemplo, Administración, Área Gubernamental, Área Personal).
  2. Navegación Secundaria (nav-secondary.tsx): Renderiza enlaces de utilidad o meta-enlaces (por ejemplo, “Acerca del Autor”, “Acerca del Proyecto”).

sidebar

Lamentablemente el snippet de código es medio largo, te invito a chequear el código del componente en el repo.

Tip pro: Agregá tooltips para rutas con nombres largos que puedan truncarse:

tooltip

Con el sidebar definido, necesitábamos componentes simples y utilitarios para la parte superior e inferior de la ventana principal.

Dark Mode

Laburar con interfaces blanco brillante en sesiones largas de código te quema los ojos. Integrar el dark mode ahora es una feature obligatoria para cualquier aplicación moderna.

Siguiendo la guía de dark mode de shadcn/ui , coloqué el toogle en la esquina superior derecha del Header. Esta ubicación es la convención universalmente aceptada y requiere la menor carga cognitiva para los usuarios que buscan cambiar el tema.

Aplicando el Layout Globalmente

Ahora que tenemos todas las piezas, vamos a componerlas en el layout principal src/app/layout.tsx.

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";

import "./globals.css";

import { AppFooter } from "@/components/layout/app-footer";
import { AppHeader } from "@/components/layout/app-header";
import { AppSidebar } from "@/components/layout/app-sidebar";
import { ThemeProvider } from "@/components/theme/theme-provider";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Municipal Services",
  description:
    "Access municipal services, submit requests, and manage your civic obligations through our secure online platform.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <SidebarProvider>
            <AppSidebar />
            <SidebarInset>
              <AppHeader />
              <main className="flex flex-1 flex-col gap-4 p-4 pt-0">
                {children}
              </main>
              <AppFooter />
            </SidebarInset>
          </SidebarProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}

Vas a notar que con el sidebar, el header y el footer, el espaciado vertical puede sentirse un poco apretado. Ajustemos el componente src/components/layout/page-layout.tsx que creamos en el blog anterior:

Antes:

export function PageLayout({ children, className }: PageLayoutProps) {
  return (
    <div
      className={cn("min-h-screen bg-background text-foreground", className)}
    >
      <div className="py-16 md:py-24">
        <div className="max-w-7xl mx-auto px-4">{children}</div>
      </div>
    </div>
  );
}

Ahora:

export function PageLayout({ children, className }: PageLayoutProps) {
  return (
    <div className={cn("bg-background text-foreground", className)}>
      <div className="max-w-7xl mx-auto px-4 py-8">{children}</div>
    </div>
  );
}

El Resultado

result

Ahora tenemos:

La base de la UI es sólida y fácil de mantener. A medida que agregues nuevas rutas, simplemente marcalas con sidebarContent y aparecerán automáticamente en la navegación.

¿Qué Sigue?

Antes de meternos de lleno a construir las features reales de nuestro proyecto de software grande, es un momento ideal para estabilizar el sistema a través de testing. Nuestra próxima fase se centrará en pruebas unitarias y de integración para asegurar que todas estas partes móviles recién creadas funcionen según lo previsto y se mantengan estables a medida que el proyecto crece.

Pero ese es tema para el próximo post. Por ahora, ¡a disfrutar el dark mode y el layout profesional! 🎨

Próximo Post: Proyectos de Software Grandes: Testing Unitario

Hey, check me out!

You can find me here