Files
aggios.app/1. docs/nova-interface.md

5.5 KiB

System Instruction: Arquitetura de Layout com Sidebar Expansível

Role: Senior React Developer & UI Specialist Tech Stack: React, Tailwind CSS (Sem bibliotecas de ícones ou fontes externas).

Objetivo: Implementar um sistema de layout "Dashboard" composto por um Menu Lateral (Sidebar) que expande e colapsa suavemente e uma área de conteúdo principal.

Requisitos Críticos de Animação:

  1. A transição de largura da sidebar deve ser suave (transition-all duration-300).
  2. O texto dos botões não deve quebrar ou desaparecer bruscamente. Use a técnica de transição de max-width e opacity para que o texto deslize suavemente para fora.
  3. Não utilize bibliotecas de animação (Framer Motion, etc), apenas Tailwind CSS puro.

1. Componente: DashboardLayout.tsx (Container Principal)

Este componente deve gerenciar o estado global do menu (aberto/fechado) para evitar "prop drilling" desnecessário.

import React, { useState } from 'react';
import { SidebarRail } from './SidebarRail';

interface DashboardLayoutProps {
  children: React.ReactNode;
}

export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
  // Estado centralizado do layout
  const [isExpanded, setIsExpanded] = useState(true);
  const [activeTab, setActiveTab] = useState('home');

  return (
    <div className="flex h-screen w-full bg-gray-900 text-slate-900 overflow-hidden p-3 gap-3">
      {/* Sidebar controla seu próprio estado visual via props */}
      <SidebarRail 
        activeTab={activeTab} 
        onTabChange={setActiveTab}
        isExpanded={isExpanded}
        onToggle={() => setIsExpanded(!isExpanded)}
      />

      {/* Área de Conteúdo (Children) */}
      <main className="flex-1 h-full min-w-0 overflow-hidden flex flex-col bg-white rounded-3xl shadow-xl relative">
        {children}
      </main>
    </div>
  );
};

2. Componente: SidebarRail.tsx (Lógica de Animação)

Aqui reside a lógica visual. Substitua os ícones por <span>Icon</span> ou SVGs genéricos para manter o código agnóstico.

Pontos de atenção no código abaixo:

  • w-[220px] vs w-[72px]: Define a largura física.
  • max-w-[150px] vs max-w-0: Define a animação do texto.
  • whitespace-nowrap: Impede que o texto pule de linha enquanto fecha.
import React from 'react';

interface SidebarRailProps {
  activeTab: string;
  onTabChange: (tab: string) => void;
  isExpanded: boolean;
  onToggle: () => void;
}

export const SidebarRail: React.FC<SidebarRailProps> = ({ activeTab, onTabChange, isExpanded, onToggle }) => {
  return (
    <div 
      className={`
        h-full bg-zinc-900 rounded-3xl flex flex-col py-6 gap-4 text-gray-400 shrink-0 border border-white/10 shadow-xl 
        transition-[width] duration-300 ease-[cubic-bezier(0.25,0.1,0.25,1)] px-3
        ${isExpanded ? 'w-[220px]' : 'w-[72px]'}
      `}
    >
      {/* Header / Toggle */}
      <div className={`flex items-center w-full relative transition-all duration-300 mb-4 ${isExpanded ? 'justify-between px-1' : 'justify-center'}`}>
        <div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white font-bold shrink-0 z-10">
          Logo
        </div>
        
        {/* Título com animação de opacidade e largura */}
         <div className={`overflow-hidden transition-all duration-300 ease-in-out whitespace-nowrap absolute left-14 ${isExpanded ? 'opacity-100 max-w-[100px]' : 'opacity-0 max-w-0'}`}>
            <span className="font-bold text-white text-lg">App Name</span>
         </div>
      </div>

      {/* Navegação */}
      <div className="flex flex-col gap-2 w-full">
        <RailButton 
          label="Dashboard" 
          active={activeTab === 'home'} 
          onClick={() => onTabChange('home')} 
          isExpanded={isExpanded} 
        />
        <RailButton 
          label="Settings" 
          active={activeTab === 'settings'} 
          onClick={() => onTabChange('settings')} 
          isExpanded={isExpanded} 
        />
      </div>

      {/* Footer / Toggle Button */}
      <div className="mt-auto">
        <button 
            onClick={onToggle}
            className="w-full p-2 rounded-xl hover:bg-white/10 text-gray-400 hover:text-white transition-colors flex items-center justify-center"
        >
            {/* Ícone de Toggle Genérico */}
            <span>{isExpanded ? '<<' : '>>'}</span>
        </button>
      </div>
    </div>
  );
};

// Subcomponente do Botão (Essencial para a animação do texto)
const RailButton = ({ label, active, onClick, isExpanded }: any) => (
  <button 
    onClick={onClick}
    className={`
      flex items-center p-2.5 rounded-xl transition-all duration-300 group relative overflow-hidden
      ${active ? 'bg-white/10 text-white' : 'hover:bg-white/5 hover:text-gray-200'}
      ${isExpanded ? '' : 'justify-center'}
    `}
  >
    {/* Placeholder do Ícone */}
    <div className="shrink-0 flex items-center justify-center w-6 h-6 bg-gray-700/50 rounded text-[10px]">Icon</div>
    
    {/* Lógica Mágica do Texto: Max-Width Transition */}
    <div className={`
      overflow-hidden whitespace-nowrap transition-all duration-300 ease-in-out 
      ${isExpanded ? 'max-w-[150px] opacity-100 ml-3' : 'max-w-0 opacity-0 ml-0'}
    `}>
       <span className="font-medium text-sm">{label}</span>
    </div>

    {/* Indicador de Ativo (Barra lateral pequena quando fechado) */}
    {active && !isExpanded && (
        <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-3 bg-white rounded-r-full -ml-3" />
    )}
  </button>
);