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
| Chave | Descrição |
|---|---|
header-top (default) | Header full-width no topo; sidebar e main abaixo dele. |
split-shell | Sidebar 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
| Campo | Valores | Descrição |
|---|---|---|
columns | main-only, left-main, left-main-right, main-right | Quais colunas a linha de conteúdo usa. |
header | boolean | Renderiza o header. |
footer | boolean | Renderiza o footer. |
topbarNotification | boolean | Reserva 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. |
headerSecondaryInsideHeaderOnSports | boolean | Nas rotas de esportes, renderiza o slot headerSecondary dentro do wrapper sticky do header em vez de abaixo dele. |
hideHeaderSecondaryOnSports | boolean | Nas rotas de esportes, não renderiza o headerSecondary (o sportsbook traz a própria navegação). |
contentContainer | full (default), contained | contained envelopa a linha sidebar+main em max-w-content centralizado. Só tem efeito no shell header-top. |
footerFullWidth | boolean | true renderiza o footer fora da linha de conteúdo, ocupando a largura total do viewport. |
slots — qual variante renderiza em cada posição
| Slot | Variantes disponíveis | Default |
|---|---|---|
header | header-default, header-stacked | header-default |
headerSecondary | header-secondary-nav-buttons, null | null |
sidebar | sidebar-narrow, sidebar-accordion, sidebar-tabbed, null | sidebar-narrow |
footer | footer-default, footer-stacked | footer-default |
mobileBottomNav | mobile-nav-flat, mobile-nav-fab-logo, null | mobile-nav-flat |
rightPanel | — sem variante disponível hoje; aceita apenas null | null |
banner | — sem variante registrada hoje; use null | null |
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
| Campo | Variantes | Default |
|---|---|---|
gameCard | card-default | card-default |
bannerSlide | slide-default | slide-default |
sectionTitle | section-title-default, section-title-gradient | section-title-default |
ctaButton | cta-flat, cta-signature, cta-secondary | cta-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.
sidebar — dimensões e posicionamento
| Campo | Descrição | Default |
|---|---|---|
widths.collapsed | Largura no modo ícone. Qualquer comprimento CSS. | "70px" |
widths.expanded | Largura no modo menu completo. | "280px" |
scrollBehavior | sticky (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:
- Crie o componente em
app/layouts/variants/<slot>/<NomeDaVariante>.tsx. - Adicione a chave na union correspondente em
app/types/layout.ts(ex:"header-minha-marca"emHeaderVariantKey). - Registre o componente no mapa do slot:
header,headerSecondary,sidebar→app/layouts/layout-registry.tsfooter→footerLazyLoaders, emapp/layouts/DefaultLayout.tsxmobileBottomNav→ o mapavariantsemapp/layouts/variants/mobile-bottom-nav/MobileBottomNavDispatcher.tsx
- Use a chave nova no
composition.tsda 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.