Skip to main content
Tambo360’s layout system provides a consistent, responsive structure for all authenticated pages. The layout automatically handles desktop and mobile viewports, sidebar collapsing, and navigation state.

Layout Component

The main Layout component wraps all protected routes and provides the application shell.

File Location

apps/frontend/src/components/layout/Layout.tsx

Features

Responsive Design

Automatically adapts to mobile (< 1024px) and desktop viewports

Collapsible Sidebar

Toggle between collapsed (80px) and expanded (280px) states

Mobile Overlay

Backdrop overlay when mobile menu is open

Nested Routing

Uses React Router’s <Outlet /> for child routes

Implementation

Layout.tsx
import { useState, useEffect } from 'react'
import { SidebarProvider } from '../common/sidebar'
import { AppSidebar } from './AppSidebar'
import { Outlet } from 'react-router-dom'
import { Navbar } from '../Navbar'

const LayoutContent = () => {
  const [isMobile, setIsMobile] = useState(false)
  const [showMobileMenu, setShowMobileMenu] = useState(false)
  const [isCollapsed, setIsCollapsed] = useState(false)

  useEffect(() => {
    const checkMobile = () => {
      const mobile = window.innerWidth < 1024
      setIsMobile(mobile)
      if (!mobile) setShowMobileMenu(false)
    }

    checkMobile()
    window.addEventListener('resize', checkMobile)
    return () => window.removeEventListener('resize', checkMobile)
  }, [])

  const handleToggleMenu = () => {
    if (isMobile) {
      setShowMobileMenu(!showMobileMenu)
    } else {
      setIsCollapsed(!isCollapsed)
    }
  }

  return (
    <div className="flex h-screen w-full bg-[#F8FAFC] overflow-hidden">
      {/* Mobile Overlay */}
      {isMobile && showMobileMenu && (
        <div
          className="fixed inset-0 z-[60] bg-black/40 backdrop-blur-sm lg:hidden"
          onClick={() => setShowMobileMenu(false)}
        />
      )}

      {/* Sidebar */}
      <div
        className={`h-full z-[70] transition-all duration-300 ease-in-out shrink-0 bg-white border-r border-gray-200
          ${isMobile ? 'fixed left-0 top-0 shadow-2xl' : 'relative'}
          ${isMobile && !showMobileMenu ? '-translate-x-full' : 'translate-x-0'}
          ${!isMobile ? (isCollapsed ? 'w-[80px]' : 'w-[280px]') : 'w-[280px]'}
        `}
      >
        <AppSidebar forcedCollapsed={!isMobile && isCollapsed} />
      </div>

      {/* Main Content Area */}
      <div className="flex flex-col flex-grow min-w-0 h-full">
        <Navbar onMenuClick={handleToggleMenu} />

        <main className="flex-1 overflow-y-auto">
          <div className="p-4 sm:p-6 lg:p-8 max-w-[1600px] mx-auto">
            <Outlet />
          </div>
        </main>
      </div>
    </div>
  )
}

export default function Layout() {
  return (
    <SidebarProvider>
      <LayoutContent />
    </SidebarProvider>
  )
}

Usage in Routes

The Layout component is applied to protected routes:
AppRoutes.tsx
import Layout from '../components/layout/Layout'
import ProtectedRoute from './ProtectedRoute'

<Route
  element={
    <ProtectedRoute>
      <Layout />
    </ProtectedRoute>
  }
>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/produccion" element={<Produccion />} />
  <Route path="/tambo-engine" element={<TamboEngine />} />
</Route>

AppSidebar Component

The application sidebar provides navigation to main application sections.

File Location

apps/frontend/src/components/layout/AppSidebar.tsx

Props

interface AppSidebarProps {
  forcedCollapsed?: boolean  // Controls collapsed state from parent
}

Features

  • Active Route Highlighting: Automatically highlights the current page
  • Icon-Only Mode: Shows only icons when collapsed
  • Alert Badges: Displays notification counts on TamboEngine link
  • Smooth Animations: Fade-in transitions for text elements
AppSidebar.tsx
const mainMenuItems = [
  {
    title: 'Dashboard',
    icon: LayoutDashboard,
    url: '/dashboard',
  },
  {
    title: 'Producción',
    icon: Milk,
    url: '/produccion',
  },
  {
    title: 'TamboEngine',
    icon: Cpu,
    url: '/tambo-engine',
  },
]

Implementation

AppSidebar.tsx
import { LayoutDashboard, Milk, Cpu, User } from 'lucide-react'
import { Link, useLocation } from 'react-router-dom'
import { useAuth } from '@/src/context/AuthContext'
import { useNoViewedAlerts } from '@/src/hooks/alerts/useNoViewedAlerts'

export function AppSidebar({ forcedCollapsed }: AppSidebarProps) {
  const location = useLocation()
  const { user } = useAuth()
  const { data } = useNoViewedAlerts({
    id: user.establecimientos[0].idEstablecimiento,
  })
  const isCollapsed = forcedCollapsed

  return (
    <Sidebar className="w-full border-none h-full bg-[#F1F5F9]">
      <SidebarHeader
        className={`transition-all duration-300 ${isCollapsed ? 'p-4' : 'p-8'}`}
      >
        <div className="flex items-center gap-3">
          <div className="flex h-10 w-10 shrink-0 items-center justify-center">
            <img
              src="/isotipo_tambo 1.svg"
              alt="Isotipo"
              className="h-full w-full object-contain"
            />
          </div>
          {!isCollapsed && (
            <div className="flex items-center animate-in fade-in duration-300">
              <img
                src="/logotipo 1.svg"
                alt="Tambo360"
                className="h-6 w-auto"
              />
            </div>
          )}
        </div>
      </SidebarHeader>

      <SidebarContent className="px-4 flex flex-col justify-between h-full pb-8">
        <SidebarMenu>
          {mainMenuItems.map((item) => {
            const isActive =
              item.url === '/'
                ? location.pathname === '/'
                : location.pathname.startsWith(item.url)
            return (
              <SidebarMenuItem key={item.title}>
                <SidebarMenuButton
                  asChild
                  className={`py-4 transition-all duration-200 rounded-lg ${
                    isActive
                      ? 'bg-[#D7ECAF] hover:bg-[#D7ECAF]/60 border-l-6 border-l-black'
                      : 'bg-transparent text-gray-400 hover:bg-gray-100'
                  }`}
                >
                  <Link to={item.url}>
                    <item.icon
                      className={`h-5 w-5 shrink-0 ${
                        isActive ? 'text-[#669213]' : 'text-gray-400'
                      }`}
                    />
                    {!isCollapsed && (
                      <span
                        className={`font-semibold ${
                          isActive ? 'text-[#669213]' : 'text-gray-400'
                        }`}
                      >
                        {item.title}
                        {item.url === '/tambo-engine' &&
                          data &&
                          data.cantidad > 0 && (
                            <span className="text-white bg-red-main rounded-full size-6 text-center">
                              {data.cantidad}
                            </span>
                          )}
                      </span>
                    )}
                  </Link>
                </SidebarMenuButton>
              </SidebarMenuItem>
            )
          })}
        </SidebarMenu>
      </SidebarContent>
    </Sidebar>
  )
}
The Navbar provides the top navigation bar with menu toggle and user actions.

File Location

apps/frontend/src/components/Navbar.tsx

Props

interface NavbarProps {
  onMenuClick: () => void  // Callback for hamburger menu click
}

Key Features

  • Hamburger menu button for mobile/desktop sidebar toggle
  • User profile display
  • Logout functionality
  • Establishment name display

Loading Components

The layout system includes loading states for better UX.

LoadingSpinner

Displayed during initial app load and route authentication:
LoadingSpinner.tsx
const LoadingSpinner = () => {
  return (
    <div className="flex h-screen w-full items-center justify-center bg-[#F8FAFC]">
      <div className="flex flex-col items-center gap-4">
        <div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-300 border-t-[#669213]" />
        <p className="text-sm text-gray-600">Cargando...</p>
      </div>
    </div>
  )
}
Location: apps/frontend/src/components/layout/LoadingSpinner.tsx

Loading (Full Page)

Used by ProtectedRoute during authentication check: Location: apps/frontend/src/components/layout/Loading.tsx

Responsive Breakpoints

The layout uses consistent breakpoints:
/* Mobile */
@media (max-width: 1023px) {
  - Sidebar becomes a slide-out drawer
  - Overlay backdrop appears
  - Fixed positioning
}

/* Desktop */
@media (min-width: 1024px) {
  - Sidebar is always visible
  - Can collapse to icon-only (80px)
  - No overlay needed
}

Customization

Change Sidebar Width

Layout.tsx
// Expanded width
w-[280px]  // Change to desired width

// Collapsed width
w-[80px]   // Change to desired width

Adjust Mobile Breakpoint

Layout.tsx
const checkMobile = () => {
  const mobile = window.innerWidth < 1024  // Change breakpoint here
  setIsMobile(mobile)
}

Modify Color Scheme

// Background colors
bg-[#F8FAFC]   // Main background
bg-[#F1F5F9]   // Sidebar background
bg-[#D7ECAF]   // Active item background
text-[#669213] // Active item text

Best Practices

Always use the Layout component

All authenticated pages should render inside the Layout component via <Outlet />

Don't nest Layouts

Only one Layout component should be active at a time

Mobile-first responsive design

Test all pages on mobile viewports to ensure proper sidebar behavior

Use semantic HTML

The layout uses proper HTML5 semantic elements like <nav>, <main>, and <aside>

Sidebar Primitives

Base sidebar components from common/sidebar

Protected Routes

How routes integrate with Layout

Auth Context

User data displayed in Navbar

Dashboard

Example page using Layout

Build docs developers (and LLMs) love