AppLayout
Layout completo para aplicações Next.js com sidebar, header, menu dinâmico, menu de usuário e suporte a tema dark/light.
Instalação
npm install @dexcode-core/ui react-iconsConfiguração do tema (obrigatório)
1. globals.css
Adicione o @custom-variant dark no seu arquivo de estilos globais para o Tailwind CSS v4 reconhecer o tema:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));2. layout.tsx — Script anti-flash
Para evitar o flash de tema na recarga da página, adicione um script inline síncrono no <head> do seu layout raiz. Ele lê o localStorage antes de o React hidratar e aplica a classe dark imediatamente:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: `
(function(){
try {
var t = localStorage.getItem('theme');
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch(e) {}
})();
`}} />
</head>
<body suppressHydrationWarning>
<ThemeProvider>
<AppLayout ...>
{children}
</AppLayout>
</ThemeProvider>
</body>
</html>
)
}O
suppressHydrationWarningno<html>é necessário porque a classedarkpode existir antes da hidratação do React.
Uso básico
import { ThemeProvider, AppLayout } from '@dexcode-core/ui'
import type { MenuItem } from '@dexcode-core/ui'
const menu: MenuItem[] = [
{
id: '1',
nome: 'Dashboard',
rota: '/painel',
icone: 'LuChartColumnIncreasing',
tipo: 'Link',
ordem: 1,
subMenus: [],
},
{
id: '2',
nome: 'Administração',
rota: null,
icone: 'LuSettings',
tipo: 'Label',
ordem: 2,
subMenus: [
{
id: '2-1',
nome: 'Usuários',
rota: '/painel/usuarios',
icone: 'LuUsers',
tipo: 'Link',
ordem: 1,
subMenus: [],
},
],
},
]
async function signOut() {
'use server'
// limpar sessão, redirect, etc.
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: `
(function(){
try {
var t = localStorage.getItem('theme');
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch(e) {}
})();
`}} />
</head>
<body suppressHydrationWarning>
<ThemeProvider>
<AppLayout
logoLight="/logo-light.svg"
logoDark="/logo-dark.svg"
menu={menu}
user={{ name: 'João Silva', profile: 'Administrador' }}
title="Dashboard"
onSignOut={signOut}
>
{children}
</AppLayout>
</ThemeProvider>
</body>
</html>
)
}Ícones
Os ícones do menu são carregados dinamicamente da biblioteca react-icons/lu. Passe o nome exato do ícone na propriedade icone de cada MenuItem:
{ icone: 'LuHome' } // LuHome
{ icone: 'LuSettings' } // LuSettings
{ icone: 'LuUsers' } // LuUsersConsulte todos os ícones disponíveis em react-icons.github.io (opens in a new tab).
Props — AppLayout
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
children | ReactNode | — | Conteúdo principal da página |
menu | MenuItem[] | — | Itens do menu lateral |
logoLight | string | — | Caminho da logo para o tema light |
logoDark | string | — | Caminho da logo para o tema dark |
user | AppUser | { name: 'DexCode System', profile: 'Administrador' } | Dados do usuário logado |
title | string | — | Título exibido no header desktop |
onSignOut | () => void | Promise<void> | — | Função chamada ao clicar em Sair |
Props — MenuItem
| Prop | Tipo | Descrição |
|---|---|---|
id | string | Identificador único |
nome | string | Texto exibido no menu |
rota | string | null | Rota de navegação. null para grupos (tipo Label) |
icone | string | Nome do ícone do react-icons/lu |
tipo | 'Link' | 'Label' | Link = item clicável, Label = grupo colapsável |
ordem | number | Ordem de exibição |
subMenus | MenuItem[] | Itens filhos (usado quando tipo é Label) |
Props — AppUser
| Prop | Tipo | Descrição |
|---|---|---|
name | string | Nome exibido no menu de usuário |
profile | string | Perfil/cargo exibido abaixo do nome |
email | string | E-mail (exibido se não houver profile) |
avatar | string | URL da imagem de avatar |
onSignOut com Server Action
O AppLayout é um Client Component, mas o onSignOut pode receber uma Server Action passada do layout raiz (Server Component), sem precisar tornar o layout um Client Component:
// layout.tsx (Server Component)
async function signOut() {
'use server'
// cookies().delete('session')
// redirect('/login')
}
export default function RootLayout({ children }) {
return (
<ThemeProvider>
<AppLayout onSignOut={signOut} ...>
{children}
</AppLayout>
</ThemeProvider>
)
}