5.5 KiB
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:
- A transição de largura da sidebar deve ser suave (transition-all duration-300).
- O texto dos botões não deve quebrar ou desaparecer bruscamente. Use a técnica de transição de
max-widtheopacitypara que o texto deslize suavemente para fora. - 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]vsw-[72px]: Define a largura física.max-w-[150px]vsmax-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>
);