Pular para o conteúdo principal

Layout

O layout não é um componente que você reescreve — é uma composição declarativa. Você escolhe, por chave, qual variante de cada slot (header, sidebar, footer, barra inferior mobile, …) a sua marca usa. O arquivo de entrada é app/config/layout/composition.ts.

A maioria dos forks não precisa mexer aqui. Comece por Theming — cor resolve a maior parte da identidade visual.

Layout padrão

O default do template (shell: "header-top"):

  • Header — full-width no topo: logo, navegação, busca, botões de autenticação.
  • Sidebar — coluna à esquerda, colapsável: 70px colapsada / 280px expandida.
  • Main — área de conteúdo da página.
  • Footer — dentro do main, ao final da página.
  • Barra inferior mobile — visível apenas em viewport pequeno.

No mobile (<lg) a sidebar sempre vira um drawer sobreposto, independente do shell escolhido.

O arquivo de composição

// app/config/layout/composition.ts (base)
export { defaultLayoutConfig as layoutConfig } from "~/layouts/layout.defaults";

O base apenas reexporta os defaults. A sua marca declara só o que difere, usando o helper defineLayoutConfig():

// overrides/<brand-key>/app/config/layout/composition.ts
import { defineLayoutConfig } from "~/layouts/layout.defaults";

export const layoutConfig = defineLayoutConfig({
shell: "split-shell",
structure: {
hideHeaderSecondaryOnSports: true,
},
slots: {
header: "header-stacked",
headerSecondary: "header-secondary-nav-buttons",
sidebar: "sidebar-accordion",
mobileBottomNav: "mobile-nav-fab-logo",
footer: "footer-stacked",
},
componentVariants: {
sectionTitle: "section-title-gradient",
ctaButton: "cta-signature",
},
sidebar: {
widths: { collapsed: "104px", expanded: "320px" },
},
});

:::info Este é o único config com deep-merge Overrides de marca são, em geral, substituição do arquivo inteiro — o que você não declarar fica undefined. O composition.ts é a exceção prática: como o arquivo chama defineLayoutConfig(), o objeto resultante é deep-merged com defaultLayoutConfig. Você declara apenas as chaves que quer mudar e o resto vem do base. :::

Estrutura do LayoutConfig

Os tipos vivem em app/types/layout.ts — é o arquivo autoritativo, e o TypeScript rejeita qualquer chave de variante que não exista.

shell — macro-arquitetura

ChaveDescrição
header-top (default)Header full-width no topo; sidebar e main abaixo dele.
split-shellSidebar full-height à esquerda do viewport; topbar + header + main + footer empilhados na coluna direita.

O shell só muda o layout em lg+.

structure — decisões estruturais

CampoValoresDescrição
columnsmain-only, left-main, left-main-right, main-rightQuais colunas a linha de conteúdo usa.
headerbooleanRenderiza o header.
footerbooleanRenderiza o footer.
topbarNotificationbooleanReserva o slot da barra de notificação no topo. O conteúdo vem de app/config/widgets/topbar-notifications.ts, cujo default é enabled: [] — ou seja, nada renderiza até a marca declarar os tipos que quer.
headerSecondaryInsideHeaderOnSportsbooleanNas rotas de esportes, renderiza o slot headerSecondary dentro do wrapper sticky do header em vez de abaixo dele.
hideHeaderSecondaryOnSportsbooleanNas rotas de esportes, não renderiza o headerSecondary (o sportsbook traz a própria navegação).
contentContainerfull (default), containedcontained envelopa a linha sidebar+main em max-w-content centralizado. Só tem efeito no shell header-top.
footerFullWidthbooleantrue renderiza o footer fora da linha de conteúdo, ocupando a largura total do viewport.

slots — qual variante renderiza em cada posição

SlotVariantes disponíveisDefault
headerheader-default, header-stackedheader-default
headerSecondaryheader-secondary-nav-buttons, nullnull
sidebarsidebar-narrow, sidebar-accordion, sidebar-tabbed, nullsidebar-narrow
footerfooter-default, footer-stackedfooter-default
mobileBottomNavmobile-nav-flat, mobile-nav-fab-logo, nullmobile-nav-flat
rightPanel— sem variante disponível hoje; aceita apenas nullnull
banner— sem variante registrada hoje; use nullnull

null desliga o slot inteiro (a barra inferior mobile, por exemplo, deixa de existir e não envia código nenhum pro bundle).

componentVariants — variantes de componentes reutilizados

CampoVariantesDefault
gameCardcard-defaultcard-default
bannerSlideslide-defaultslide-default
sectionTitlesection-title-default, section-title-gradientsection-title-default
ctaButtoncta-flat, cta-signature, cta-secondarycta-flat

cta-flat deriva as cores dos tokens do tema, então acompanha a sua paleta sem ajuste. cta-secondary é pensado pra ações secundárias e normalmente é aplicado por call-site, não como default da marca.

CampoDescriçãoDefault
widths.collapsedLargura no modo ícone. Qualquer comprimento CSS."70px"
widths.expandedLargura no modo menu completo."280px"
scrollBehaviorsticky (acompanha o scroll, fixa no topo) ou page (rola junto com a página)."sticky"

Mobile ignora esses campos — o drawer é o modelo único em viewport pequeno.

Criando uma variante própria

Se nenhuma das variantes existentes serve:

  1. Crie o componente em app/layouts/variants/<slot>/<NomeDaVariante>.tsx.
  2. Adicione a chave na union correspondente em app/types/layout.ts (ex: "header-minha-marca" em HeaderVariantKey).
  3. Registre o componente no mapa do slot:
    • header, headerSecondary, sidebarapp/layouts/layout-registry.ts
    • footerfooterLazyLoaders, em app/layouts/DefaultLayout.tsx
    • mobileBottomNav → o mapa variants em app/layouts/variants/mobile-bottom-nav/MobileBottomNavDispatcher.tsx
  4. Use a chave nova no composition.ts da sua marca.

O TypeScript exige uma entrada de registro pra cada chave da union, então esquecer o passo 3 é erro de compilação, não bug em runtime.

Um shell novo segue o mesmo padrão: componente em app/layouts/shells/, registro em app/layouts/shells/registry.ts, chave na union ShellArchitecture.

Observações

  • Os widgets opcionais (CTAs da sidebar, notificações de topo/rodapé, etc.) não são slots de layout — eles têm configs próprias em app/config/widgets/.
  • Trocar cores dos elementos do layout é feito nos tokens do tema (header.*, sidebar.*, footer.*), não aqui. Ver Theming.