Componentes
AppLayout

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-icons

Configuraçã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 suppressHydrationWarning no <html> é necessário porque a classe dark pode 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' }      // LuUsers

Consulte todos os ícones disponíveis em react-icons.github.io (opens in a new tab).

Props — AppLayout

PropTipoPadrãoDescrição
childrenReactNodeConteúdo principal da página
menuMenuItem[]Itens do menu lateral
logoLightstringCaminho da logo para o tema light
logoDarkstringCaminho da logo para o tema dark
userAppUser{ name: 'DexCode System', profile: 'Administrador' }Dados do usuário logado
titlestringTítulo exibido no header desktop
onSignOut() => void | Promise<void>Função chamada ao clicar em Sair

Props — MenuItem

PropTipoDescrição
idstringIdentificador único
nomestringTexto exibido no menu
rotastring | nullRota de navegação. null para grupos (tipo Label)
iconestringNome do ícone do react-icons/lu
tipo'Link' | 'Label'Link = item clicável, Label = grupo colapsável
ordemnumberOrdem de exibição
subMenusMenuItem[]Itens filhos (usado quando tipo é Label)

Props — AppUser

PropTipoDescrição
namestringNome exibido no menu de usuário
profilestringPerfil/cargo exibido abaixo do nome
emailstringE-mail (exibido se não houver profile)
avatarstringURL 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>
  )
}