Layout Composition System
O front-web-base usa um sistema de composição de layout que permite a cada brand escolher granularmente quais componentes de slot usar (header, sidebar, footer, coluna direita) — sem duplicar o DefaultLayout.tsx e sem criar forks de código para cada variação visual.
Problema resolvido
Antes do sistema de composição, cada brand que precisasse de um layout diferente teria que:
- Copiar o
DefaultLayout.tsxinteiro e modificá-lo - Manter essa cópia sincronizada com atualizações do template base
- Repetir o processo para cada slot que quisesse customizar
Com o sistema de composição, um brand configura apenas o que muda, e o DefaultLayout.tsx orquestra automaticamente com os componentes corretos.
Arquitetura
Quatro camadas independentes
A composição visual do layout existe em 4 camadas ortogonais — trocar uma não invalida as outras:
- Shell (
layoutConfig.shell) — macro-arrangement do viewport. Define se o header sticky fica full-width acima de tudo, ou se a sidebar fica full-height à esquerda com header+main empilhados na direita. Hoje:"header-top"(default),"split-shell"(replica do7k.bet.brprod). - Structure (
layoutConfig.structure) — colunas internas (main-only/left-main/left-main-right/main-right) + flags on/off:header,footer,topbarNotification,headerSecondaryInsideHeaderOnSports,hideHeaderSecondaryOnSports,contentContainer,footerFullWidth. - Slots (
layoutConfig.slots) — qual variant de cada peça está ativa:header,headerSecondary,sidebar,rightPanel,footer,banner,mobileBottomNav. - Component variants (
layoutConfig.componentVariants) — variants globais de primitivas compartilhadas:gameCard,bannerSlide,sectionTitle,ctaButton.
:::note mobileBottomNav é slot, não structure
É fácil confundir: é um slot (tem variants, default "mobile-nav-flat"), não
uma flag de structure.
:::
Marca
└── overrides/<brand-key>/app/config/layout/composition.ts ← o que a marca define
↓ defineLayoutConfig() deep merge
app/layouts/layout.defaults.ts ← defaults do base (nunca overrideable)
↓ shell + slots + componentVariants resolvem chaves
app/layouts/shells/registry.ts ← shell macro-arrangement
app/layouts/layout-registry.ts ← catálogo: string key → componente de slot
app/components/section-title/registry.ts ← variants globais de primitivas
↓ renderiza
app/layouts/shells/<Shell>.tsx ← macro layout (HeaderTopShell, SplitShell)
app/layouts/variants/<slot>/<Nome>.tsx ← componentes concretos de slot
Configuração por brand
O defineLayoutConfig(overrides) recebe um objeto parcial e faz deep merge com os defaults. O brand só precisa especificar o que diferencia da configuração base.
// app/layouts/layout.defaults.ts
export const defaultLayoutConfig: LayoutConfig = {
shell: "header-top",
structure: {
columns: "left-main",
header: true,
footer: true,
topbarNotification: true,
headerSecondaryInsideHeaderOnSports: false,
hideHeaderSecondaryOnSports: false,
contentContainer: "full",
footerFullWidth: false,
},
slots: {
header: "header-default",
headerSecondary: null,
sidebar: "sidebar-narrow",
rightPanel: null,
footer: "footer-default",
banner: null,
mobileBottomNav: "mobile-nav-flat",
},
componentVariants: {
gameCard: "card-default",
bannerSlide: "slide-default",
sectionTitle: "section-title-default",
ctaButton: "cta-flat",
},
sidebar: {
widths: { collapsed: "70px", expanded: "280px" },
scrollBehavior: "sticky",
},
};
export function defineLayoutConfig(overrides: DeepPartial<LayoutConfig>): LayoutConfig {
return deepMerge(defaultLayoutConfig, overrides);
}
:::caution defineLayoutConfig faz deep merge; override de arquivo NÃO
O deep merge vale só dentro de defineLayoutConfig. O
brandOverridesPlugin que resolve overrides/<brand>/app/config/layout/composition.ts
substitui o arquivo inteiro — não há merge campo-a-campo entre o arquivo do
base e o da marca.
:::
Shell architecture (macro-layout)
Shells vivem em app/layouts/shells/. Cada um implementa o contrato ShellProps (topbar, header, sidebar, rightPanel, footer, refs, mobile state, sports flags) e é resolvido via app/layouts/shells/registry.ts.
app/layouts/shells/
├── shell-props.ts — interface ShellProps (contrato compartilhado)
├── registry.ts — Partial<Record<ShellArchitecture, ShellComponent>>
├── HeaderTopShell.tsx — shell default (header sticky no topo, sidebar+main embaixo)
├── SplitShell.tsx — shell 7k (sidebar full-height, header+main à direita)
└── MainContent.tsx — helper compartilhado (sports flags + footer placement)
Como adicionar shell novo: drop <NewShell>.tsx em app/layouts/shells/ implementando ShellProps, registra em registry.ts, estende ShellArchitecture em app/types/layout.ts. Brand opt-in via shell: "<key>". Slots, columns, e variants continuam funcionando dentro do novo shell.
Mobile é universal: independente do shell, sidebar vira drawer overlay e header sticky no topo. Shell só altera comportamento em lg+.
Sidebar variants são shell-agnósticas. Consomem CSS custom properties publicadas pelo shell ativo:
| Var | header-top | split-shell |
|---|---|---|
--sidebar-top | var(--header-h) | 0 |
--sidebar-h | calc(100dvh - var(--header-h) - var(--topbar-visible-h)) | 100dvh |
--sidebar-w-collapsed / --sidebar-w-expanded | brand-level (de layoutConfig.sidebar.widths) | idem |
Variant não conhece o shell ativo nem os widths brand — só lê os vars. Permite criar shells novos sem refactorar sidebar.
Auxiliary widgets escapam o shell via position: fixed: BackToTop, BottomNotification, LastGameFloatingWidget, UpdateBanner, MobileBottomNav, modais (auth/deposit/kyc), UserPanel slide-out, fullscreen game iframe. Independem do shell ativo.
HeaderSecondary slot — barra contextual abaixo do header sticky
Quando a brand precisa de "barra principal sticky + faixa de navegação logo abaixo, em fluxo normal", a navegação não deve ser empurrada pra baixo dentro do mesmo componente de header. Se for, a faixa secundária vira sticky junto porque o shell mede a zona inteira em data-layout-header.
Convenção:
slots.headercontinua sendo a barra sticky principal.slots.headerSecondaryé a faixa logo abaixo do header, renderizada fora da zona sticky.- A navegação compartilhada vive em
app/components/layout/HeaderNav.tsxcomappearance: "pill" | "tab" | "button". - Variants ficam em
app/layouts/variants/header-secondary/(ex:HeaderSecondaryNavButtons.tsx). - Conteúdo contextual (Slots/Live/Crash Games em rota cassino, esportes em rota sports, etc.) é resolvido por
app/layouts/variants/header-secondary/resolve.tslendoapp/config/layout/header-secondary-nav.ts. - Config aceita 3 fontes:
static(link declarado direto),casino-sidebar(reusa item decasinoItemspori18nKey),sports-sidebar(reusa item desportsMainItems/topSportsItemsporslug).
Combinações comuns:
header-default+headerSecondary: null→ comportamento históricoheader-stacked+header-secondary-nav-buttons→ padrão 7k legado
HeaderStacked.tsx é uma casca genérica em cima do HeaderDefault: sem nav inline e sem logo desktop, mas mantém logo mobile.
Component variants — primitivas compartilhadas brand-globais
Algumas primitivas têm variants escolhidos globalmente por brand, não por consumer individual. Vivem em layoutConfig.componentVariants e são distribuídos pela árvore via ComponentVariantsProvider (app/context/component-variants.tsx):
componentVariants: {
gameCard: "card-default",
bannerSlide: "slide-default",
sectionTitle: "section-title-default" | "section-title-gradient",
ctaButton: "cta-flat",
}
As chaves válidas de sectionTitle são só essas duas (SectionTitleVariantKey em
app/types/layout.ts) — não existe section-title-panel nem um componente
SectionTitlePanel.
A primitive SectionTitle em app/components/section-title/ concentra shell, ícone, heading semântico (h2/h3), setas de carousel e CTA de "ver todos". Headers de rows/widgets que seguem essa família visual delegam pra SectionTitle em vez de criar markup bespoke. Exceções por seção ficam em app/config/widgets/section-titles.ts (defaults + byId por row.slug ou id sintético).
Naming: variants usam nome de padrão visual, não de brand (ex: SectionTitleGradient / section-title-gradient, NÃO SectionTitle7K). Reusável por qualquer brand futura.
Registries — cinco mapas separados, não um objeto aninhado
app/layouts/layout-registry.ts exporta cinco registries independentes
(Record<VariantKey, ComponentType>), não um layoutRegistry aninhado:
// app/layouts/layout-registry.ts
export const headerRegistry: Record<HeaderVariantKey, ComponentType> = {
"header-default": HeaderDefault,
"header-stacked": HeaderStacked,
};
/** Quais headers já embutem input de busca no header sticky. */
export const headerEmbedsSearchInput: Record<HeaderVariantKey, boolean> = {
"header-default": false,
"header-stacked": true,
};
export const headerSecondaryRegistry: Record<HeaderSecondaryVariantKey, ComponentType> = {
"header-secondary-nav-buttons": HeaderSecondaryNavButtons,
};
export const sidebarRegistry: Record<SidebarVariantKey, ComponentType> = {
"sidebar-narrow": SidebarNarrow,
"sidebar-accordion": SidebarAccordion,
"sidebar-tabbed": SidebarTabbed,
};
/** Sem variantes hoje (`RightPanelVariantKey` é `never`) — infra mantida. */
export const rightPanelRegistry = {} as Record<RightPanelVariantKey, ComponentType>;
/** Sem variantes hoje. */
export const bannerRegistry = {} as Record<BannerVariantKey, ComponentType>;
TypeScript força uma entrada pra cada chave do union correspondente, então
esquecer de registrar é erro de compilação. O mesmo vale pro
headerEmbedsSearchInput: adicionar um header novo obriga a registrar a decisão
sobre busca embutida.
Registry eager × lazy-loader map — a decisão arquitetural
footer e mobileBottomNav NÃO têm registry. São resolvidos por mapas de
lazy-loader por variante nos próprios dispatchers:
| Slot | Como resolve | Onde |
|---|---|---|
| header, headerSecondary, sidebar, rightPanel | registry eager | layout-registry.ts |
| footer | mapa lazy | footerLazyLoaders em DefaultLayout.tsx |
| mobileBottomNav | mapa lazy | lazyLoaders em MobileBottomNavDispatcher.tsx |
O motivo: o registry eager traz todas as variantes como dependência estática
de _layout, então toda marca baixa o chunk de todas elas. Isso só é aceitável
pra slots cujas variantes são pequenas e que sempre renderizam em desktop +
mobile. Footer e mobile-bottom-nav não se encaixam, então usam mapa lazy — cada
marca baixa só o chunk da variante ativa, e marca com slot = null baixa zero.
A segurança de tipo não se perde: o tipo FooterVariantKey /
MobileBottomNavVariantKey em layoutConfig.slots já garante validade em compile
time — registry não é necessário pra isso.
A regra deve se estender a
headerRegistry/sidebarRegistryquando eles virarem mapas lazy (verdocs/superpowers/specs/2026-05-02-bundle-size-audit.md).
DefaultLayout como orchestrator
O DefaultLayout.tsx lê o layoutConfig do context e indexa os registries:
// app/layouts/DefaultLayout.tsx (simplificado)
const { slots, structure } = useLayoutConfig();
const Header = headerRegistry[slots.header];
const Sidebar = sidebarRegistry[slots.sidebar];
const RightPanel = slots.rightPanel ? rightPanelRegistry[slots.rightPanel] : null;
Invariante: o DefaultLayout só indexa um registry quando o slot
correspondente não é null. É isso que torna rightPanelRegistry e
bannerRegistry vazios seguros.
Estrutura de pastas
app/
├── config/
│ └── layout/
│ ├── composition.ts ← brand-editável (overrideable)
│ └── header-secondary-nav.ts ← config da faixa secundária
├── layouts/
│ ├── DefaultLayout.tsx ← orchestrator (não overrideable) + footerLazyLoaders
│ ├── layout.defaults.ts ← defaultLayoutConfig + defineLayoutConfig()
│ ├── layout-registry.ts ← os 5 registries eager
│ ├── layout-component.ts
│ ├── shells/ ← macro-layout (HeaderTopShell, SplitShell)
│ └── variants/
│ ├── header/ ← HeaderDefault, HeaderStacked
│ ├── header-secondary/ ← HeaderSecondaryNavButtons + resolve.ts
│ ├── sidebar/ ← SidebarNarrow, SidebarAccordion, SidebarTabbed
│ ├── footer/ ← FooterDefault, FooterStacked (lazy)
│ └── mobile-bottom-nav/ ← família de slot (ver abaixo)
└── types/
└── layout.ts ← LayoutConfig, ColumnLayout, *VariantKey
O import do config de composição é ~/config/layout/composition — não existe
~/config/layout.config nem diretório app/layouts/topbar/.
Regra importante: app/layouts/ e app/types/layout.ts são maquinário estrutural — nunca fazem parte da whitelist de overrides. Brands customizam apenas app/config/layout/composition.ts.
Slot family: mobile-bottom-nav
app/layouts/variants/mobile-bottom-nav/ é uma família completa, não uma variante
solta:
| Arquivo | Papel |
|---|---|
MobileBottomNavDispatcher.tsx | Resolve a variante via mapa lazy local |
MobileBottomNavFlat.tsx | Variante mobile-nav-flat (default) |
MobileBottomNavFabLogo.tsx | Variante com FAB central |
intents.ts / resolve.ts | Intenções de navegação e resolução |
handlers.ts / specials.ts | Handlers e casos especiais |
_shared/ | Peças compartilhadas entre variantes |
O default é slots.mobileBottomNav: "mobile-nav-flat"; marca que não quer a barra
usa null e não baixa nada.
Como adicionar uma nova variante
1. Criar o componente
// app/layouts/variants/header/HeaderCustom.tsx
export function HeaderCustom() {
// implementação da nova variante de header
return <header>...</header>;
}
2. Adicionar a chave de tipo
// app/types/layout.ts
export type HeaderVariantKey =
| "header-default"
| "header-stacked"
| "header-custom"; // ← nova chave
3. Registrar no registry
// app/layouts/layout-registry.ts
import { HeaderCustom } from "./variants/header/HeaderCustom";
export const headerRegistry: Record<HeaderVariantKey, ComponentType> = {
"header-default": HeaderDefault,
"header-stacked": HeaderStacked,
"header-custom": HeaderCustom, // ← novo registro
};
// Header novo obriga a decidir isto também (é erro de compilação sem a entrada):
export const headerEmbedsSearchInput: Record<HeaderVariantKey, boolean> = {
"header-default": false,
"header-stacked": true,
"header-custom": false, // ← decisão registrada
};
Se o slot for footer ou mobileBottomNav, o passo 3 é diferente: adicione ao
mapa lazy do dispatcher correspondente, não a um registry eager.
4. Usar na marca
// overrides/<brand-key>/app/config/layout/composition.ts
import { defineLayoutConfig } from "~/layouts/layout.defaults";
export const layoutConfig = defineLayoutConfig({
slots: {
header: "header-custom",
},
});
Como configurar o layout de um brand
O brand precisa criar um arquivo de override em:
overrides/<brand-key>/app/config/layout/composition.ts
O arquivo deve usar defineLayoutConfig e especificar apenas o que diferencia da configuração padrão:
import { defineLayoutConfig } from "~/layouts/layout.defaults";
// Exemplo real: a família 7k usa o shell split + header empilhado
export const layoutConfig = defineLayoutConfig({
shell: "split-shell",
slots: {
header: "header-stacked",
headerSecondary: "header-secondary-nav-buttons",
sidebar: "sidebar-narrow",
},
});
O Vite plugin de overrides redireciona automaticamente ~/config/layout/composition para o arquivo da marca quando o ORIGIN_DOMAIN bate com o <brand-key>.
Exemplo: shell split (família 7k)
O shell split-shell põe a sidebar full-height à esquerda, com header e main
empilhados à direita:
┌─────────────┬───────────────────────────┐
│ │ Header (stacked) │
│ ├───────────────────────────┤
│ Sidebar │ HeaderSecondary (nav) │
│ (narrow, ├───────────────────────────┤
│ full-height)│ main (flex-1) │
│ │ {children} │
└─────────────┴───────────────────────────┘
Config: shell: "split-shell", slots.header = "header-stacked",
slots.headerSecondary = "header-secondary-nav-buttons"
:::note Coluna direita não tem variantes hoje
structure.columns aceita "left-main-right" e "main-right", mas
RightPanelVariantKey é never — não há variante de painel direito registrada. A
infra existe pra quando alguma marca precisar.
:::
Widgets — UI opcional embebida em slots
Além dos slots estruturais (header, sidebar, footer, banner, right-panel), o template tem um conceito paralelo de widgets: blocos de UI que a brand pode ligar/desligar e parametrizar via app/config/widgets/<nome>.ts. Cada widget tem seu próprio variant (visual) + items[] (conteúdo) + opções específicas.
A diferença com os slots:
- Slot (
composition.ts) decide o esqueleto da página — header sempre existe, sidebar sempre existe, etc. - Widget (
widgets/<nome>.ts) é opcional — brand definevariant: nullpra desligar inteiro, ou injeta items/cores.
Widgets atuais
A lista viva é app/widgets/ (18 diretórios hoje) + app/config/widgets/. Não
mantenha um inventário aqui — ele rota. Os quatro abaixo são os exemplos
canônicos do padrão:
| Widget | Config | Componente | Por que é referência |
|---|---|---|---|
| Sidebar Buttons | app/config/widgets/sidebar-buttons.ts | app/widgets/sidebar-buttons/ | O mais elaborado — variants + catálogo typed de intents + cores per-item. Detalhado abaixo |
| Top Games | app/config/widgets/top-games.ts | app/widgets/top-games/ | Row com múltiplas variants visuais: outline-sideways, showcase, corner-diagonal |
| Home Banner | app/config/widgets/home-banner.ts | app/widgets/home-banner/ | Hero da home — caso canônico de preload sincronizado com config (ver Performance) |
| Home Leagues | app/config/widgets/home-leagues.ts | app/widgets/home-leagues/ | Variants default / square + ícones string-driven de config |
Os demais hoje: bottom-notifications, campaign-widget, featured-game,
game-assistant, horizontal-menu, profile, profiles-home-row,
quick-access-menu, recommended-games, shared, sidebar-profiles, stories,
top-10-games, topbar-notifications.
Sidebar Buttons widget — pattern canônico
O sidebar-buttons é o widget mais elaborado e serve de referência pro pattern de widgets configuráveis no projeto. Tem 3 variants visuais e um catálogo typed de "intents":
// app/types/sidebar-buttons.ts
export type SidebarButtonsVariantKey = "colored" | "gradient" | "grid";
export type SidebarButtonIntent =
| "casino" | "sports" | "tournaments" | "missions"
| "referral" | "rewards" | "mini-games" | "promotions";
export interface SidebarButtonsConfig {
variant: SidebarButtonsVariantKey | null; // null = widget desliga
items: SidebarButtonItem[];
decoration?: string; // SVG watermark (só usado pelo variant `grid`)
}
Visual variants:
| Variant | Look | Quando usar |
|---|---|---|
colored | Pilha vertical de pills full-width com gradient diagonal + sublabel opcional ("Participe dos / Torneios") | Default do base — destaque alto, mostra bônus dinâmico no referral |
gradient | Pills compactas (rounded-lg, p-2) com gradient horizontal accent → muted, ícone Lucide + chevron | Estilo 7K — clean, integrado com o resto da nav |
grid | 3-col grid de tiles com 3D illustrations + decoration SVG | Estilo Vera — visual rico, requer assets PNG |
Items podem ser:
- Catalog (
{ intent: "tournaments" }) — typed enum, rota canônica protegida, label/icon defaults do core - Custom (
{ custom: true, label, href, ... }) — escape hatch pra CTAs únicos da brand - Special (
{ type: "sponsor-cta" }) — componente dedicado pra dados que não cabem em config estático (ex: sponsor com srcSet)
Brand controla cores per-item:
// overrides/<brand>/app/config/widgets/sidebar-buttons.ts
{
intent: "referral",
iconType: "lucide",
icon: Users,
gradient: ["#FF4606", "#13051c"], // accent → sidebar bg
}
Tres tiers de prioridade (do mais específico pro mais genérico):
gradient: [from, to]— full override de ambos stopscolor: "#hex"— só accent, end usa fallback do variant- Nenhum — variant usa o default brand-aware via Tailwind theme tokens (
from-sidebar-button-bg to-sidebar-bgno gradient variant;from-bg-secondary to-bg-primaryno grid)
Por que matiz constante no fade-to-near-black: o pattern [medium-tone, near-black-same-hue] (ex: ["#761821", "#280505"] red wine) cria depth sem virar uma transição cor-pra-cor dissonante. Mantém só luminosidade caindo (~45% → ~7%), preservando identidade.
Brand-aware via theme tokens (default): se o brand não passa gradient/color, o variant usa Tailwind classes que puxam do tema do brand (bg-sidebar-button-bg, bg-sidebar-bg). Cada brand vê o widget pintado nas suas próprias cores automaticamente — sem precisar override per-item.
Nota histórica —
important: true: o projeto não usa maisimportant: truenotailwind.config.js(removido em 2026-04-29, commitec8ef0eae, CSS -23.5KB raw). A precedência voltou ao normal: inline style vence utility class. O padrãothemeBgClassque você vê emSidebarButtonsGradient.tsx/SidebarButtonsGrid.tsx(omitir a classe default quando há override inline) era workaround pra aquela inversão e não é mais necessário — o inline style já ganha sozinho. Ver Performance.
Estrutura app/widgets/
Paralelo a app/layouts/, mas pra widgets opcionais:
app/widgets/
├── sidebar-buttons/ ← NOVO widget pattern
│ ├── SidebarButtons.tsx ← dispatcher (lê config + delega)
│ ├── SidebarButtonsColored.tsx ← variant component
│ ├── SidebarButtonsGradient.tsx ← variant component
│ ├── SidebarButtonsGrid.tsx ← variant component
│ ├── intents.ts ← INTENT_DEFAULTS (catalog)
│ ├── resolve.ts ← helper resolve item → flat shape
│ ├── registry.ts ← variant key → component
│ ├── referral-helpers.ts ← hooks compartilhados
│ ├── specials/ ← componentes dedicados (sponsor-cta)
│ └── __tests__/ ← unit tests
├── topbar-notifications/ ← migrado de app/components/layout/
├── bottom-notifications/ ← idem
├── campaign-widget/ ← migrado de app/components/campaign/
└── home-leagues/ ← migrado de app/components/sports/
Domain primitives — variants in-place, config compartilhado
Componentes que são primitivas de domínio (usados em N superfícies do produto, ex: GameCard, ProviderCard) podem ganhar variant system + config sem sair do folder de domínio. Reserve app/widgets/ pra widgets de página opcionais (sidebar-buttons, top-games, topbar-notifications). Pra primitivas de domínio:
- Componente fica onde está (ex:
app/components/games/GameCard.tsx). - Variants ao lado do dispatcher (
<Variant>.tsxno mesmo folder) +<nome>-registry.ts. - Tipos centralizados em
app/types/<nome>.ts. - Config canônica em
app/config/widgets/<nome>.ts— mesmo que o componente não seja "widget", o folderwidgets/é o pattern do projeto pra "componente UI configurável". - Brand override em
overrides/<brand>/app/config/widgets/<nome>.ts.
Isso preserva imports existentes em consumidores múltiplos (mover quebraria N arquivos sem ganho). Exemplos shipped:
| Primitive | Folder | Variants | Config |
|---|---|---|---|
GameCard | app/components/games/ | classic (default), stacked (legado 7k pill) | app/config/widgets/game-card.ts |
ProviderCard | app/components/home/ | classic (default), logo-only (legado 7k card wider-than-tall) | app/config/widgets/provider-card.ts |
GameStats / GameWinners | app/components/games/ | sem variants — só campo decoration por brand | app/config/widgets/game-stats-card.ts + game-winner-card.ts |
Asset configurável > CSS-only pra decoração brandizada
Quando o stakeholder espera elemento decorativo brand-specific (logo recortado, watermark, ilustração), o componente expõe campo decoration no config widget:
type WidgetDecoration =
| { kind: "none" }
| { kind: "asset"; src: string; alt?: string };
Variant renderiza <img> quando kind === "asset". Cada brand cria asset em overrides/<brand>/public/assets/widgets/<nome>/decoration.<ext>. Default = none.
Não usar CSS-only (clip-path, linear-gradient em pseudo-element) pra reproduzir decorações que o stakeholder espera ver brandizadas — limita reuse cross-brand. Pattern canônico em app/types/top-games.ts (TopGamesDecoration); replicado em game-stats-card, game-winner-card.
Lógica compartilhada entre variants → util
Quando duas variants de uma primitiva compartilham resolução não-trivial (ex: chain de fallback de stats no GameCard), extrai pra um util ao lado: <componente>-balloon.ts, <componente>-resolve.ts, etc. Util fica no mesmo folder do componente. Evita duplicação e drift entre variants.
File-replacement — regra de replicação em overrides
O brandOverridesPlugin do Vite faz file-replacement, não deep-merge — quando overrides/<brand>/app/config/<dir>/X.ts existe, ele substitui inteiro o arquivo do base. Não há merge campo-a-campo.
Consequência prática: sempre que uma refac mudar o shape de uma config overridable (adicionar/remover/renomear campo, trocar tipo, mudar enum), aplicar a mudança em todos os overrides que a brand tiver do arquivo em questão. Senão, brands com override antigo:
- Vão quebrar em runtime (campo agora obrigatório, override não tem)
- Ou ficar com default silencioso (campo opcional, override não tem →
undefinedem runtime)
Checklist antes de fechar uma refac de config:
# 1. Encontrar overrides que mencionam o campo antigo/novo
grep -rn "nomeDoCampo" overrides/ app/config/
# 2. Garantir que nenhum ficou desalinhado
pnpm typecheck:ci && pnpm quality
Se a mudança é deletar um campo e o valor era igual ao default em todas as brands, pode-se propor remover o override inteiro (brand herda default) — mas é decisão do dev, não unilateral.
Regra simples: quando criar campo novo num config com overrides, copia o default pra todos os overrides mesmo quando o valor bate com o default. Mantém explícito e evita surpresa quando o default mudar no futuro.
Separação de responsabilidades
| Pasta | Overrideable | Propósito |
|---|---|---|
app/config/ | ✅ Sim | Brand config — o que varia por marca. Subdividida em subpastas semânticas por domínio (22 hoje — app/config/ é a lista viva). Imports usam ~/config/<dir>/X, sem sufixo .config |
app/layouts/ | ❌ Não | Maquinário estrutural dos slots (header, sidebar, footer, banner, right-panel) |
app/widgets/ | ❌ Não | Componentes dos widgets opcionais embebidos. Brand controla via app/config/widgets/<nome>.ts |
app/router/ | ❌ Não | Registro de rotas (brands customizam via app/config/routes/paths.ts) |
app/types/ | ❌ Não | Tipos compartilhados — configs e overrides importam daqui |
Sidebar variants disponíveis
Os três registrados em sidebarRegistry:
| Key | Look | Quando usar |
|---|---|---|
sidebar-narrow | Default. Sections "flat" (label + chevron + items abaixo, dividers entre sections) | Maioria das marcas |
sidebar-accordion | Sections como accordions (header clicável + chevron rotacional + body smooth-height via grid-template-rows: 0fr → 1fr). Items em pill gradient (bg-gradient-to-r from-sidebar-button-bg to-sidebar-bg). Comportamento route-aware: rota cassino abre casino + fecha sports; rota sports faz o inverso; home preserva escolha manual | Marcas com look 7k legado (cl-bet7k-com, 7k-bet-br) |
sidebar-tabbed | Sections em abas | Marcas que preferem navegação por aba a accordion |
:::note SidebarWide.tsx existe no disco mas não está registrado
O arquivo app/layouts/variants/sidebar/SidebarWide.tsx continua presente, mas
sidebarRegistry não tem a chave sidebar-wide — não há como ativá-lo por
config. Trate como não disponível; se precisar do comportamento, registre a chave
primeiro (e adicione ao SidebarVariantKey).
:::
Lógica de data layer (sections, sportsNavItems, popularItems, topSportsNavItems, restrictedAccountSection) é compartilhada via hook useSidebarSections (app/layouts/variants/sidebar/hooks/useSidebarSections.ts). Items abaixo das sections (promotions, support, blog, FAQ, install-app, telegram, affiliates, responsible-gaming, debug) são compartilhados via componente <SidebarBottomItems style="default" | "pill" />.
Documentação relacionada
- Architecture — Icon system —
unplugin-icons+ Iconify (lucide / simple-icons / mdi) + custom SVGs - Architecture — Performance PSI — checklist de LCP / FCP / CLS / TBT pra mudanças above-the-fold
- Architecture — Imagens — perfil único da arte de jogo (relevante pro
gameCard) - Architecture — Trunks de marca — antes de portar uma variante pra outra trunk
- Template — Layout — comportamento dos componentes de variante (header, sidebar, footer)
- Pontos de Customização Rápida — como configurar o
composition.tsde uma marca - Arquitetura — app/config — o que pode e não pode entrar em
app/config/