149 lines
5.5 KiB
Markdown
149 lines
5.5 KiB
Markdown
# 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.
|
|
|
|
```tsx
|
|
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.
|
|
|
|
```tsx
|
|
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>
|
|
);
|
|
``` |