Pular para o conteúdo principal

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:

  1. Copiar o DefaultLayout.tsx inteiro e modificá-lo
  2. Manter essa cópia sincronizada com atualizações do template base
  3. 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:

  1. 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 do 7k.bet.br prod).
  2. Structure (layoutConfig.structure) — colunas internas (main-only / left-main / left-main-right / main-right) + flags on/off: header, footer, topbarNotification, headerSecondaryInsideHeaderOnSports, hideHeaderSecondaryOnSports, contentContainer, footerFullWidth.
  3. Slots (layoutConfig.slots) — qual variant de cada peça está ativa: header, headerSecondary, sidebar, rightPanel, footer, banner, mobileBottomNav.
  4. 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:

Varheader-topsplit-shell
--sidebar-topvar(--header-h)0
--sidebar-hcalc(100dvh - var(--header-h) - var(--topbar-visible-h))100dvh
--sidebar-w-collapsed / --sidebar-w-expandedbrand-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.header continua 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.tsx com appearance: "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.ts lendo app/config/layout/header-secondary-nav.ts.
  • Config aceita 3 fontes: static (link declarado direto), casino-sidebar (reusa item de casinoItems por i18nKey), sports-sidebar (reusa item de sportsMainItems / topSportsItems por slug).

Combinações comuns:

  • header-default + headerSecondary: null → comportamento histórico
  • header-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:

SlotComo resolveOnde
header, headerSecondary, sidebar, rightPanelregistry eagerlayout-registry.ts
footermapa lazyfooterLazyLoaders em DefaultLayout.tsx
mobileBottomNavmapa lazylazyLoaders 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/sidebarRegistry quando eles virarem mapas lazy (ver docs/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/compositionnã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:

ArquivoPapel
MobileBottomNavDispatcher.tsxResolve a variante via mapa lazy local
MobileBottomNavFlat.tsxVariante mobile-nav-flat (default)
MobileBottomNavFabLogo.tsxVariante com FAB central
intents.ts / resolve.tsIntenções de navegação e resolução
handlers.ts / specials.tsHandlers 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 define variant: null pra 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:

WidgetConfigComponentePor que é referência
Sidebar Buttonsapp/config/widgets/sidebar-buttons.tsapp/widgets/sidebar-buttons/O mais elaborado — variants + catálogo typed de intents + cores per-item. Detalhado abaixo
Top Gamesapp/config/widgets/top-games.tsapp/widgets/top-games/Row com múltiplas variants visuais: outline-sideways, showcase, corner-diagonal
Home Bannerapp/config/widgets/home-banner.tsapp/widgets/home-banner/Hero da home — caso canônico de preload sincronizado com config (ver Performance)
Home Leaguesapp/config/widgets/home-leagues.tsapp/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.

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:

VariantLookQuando usar
coloredPilha 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
gradientPills compactas (rounded-lg, p-2) com gradient horizontal accent → muted, ícone Lucide + chevronEstilo 7K — clean, integrado com o resto da nav
grid3-col grid de tiles com 3D illustrations + decoration SVGEstilo 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):

  1. gradient: [from, to] — full override de ambos stops
  2. color: "#hex" — só accent, end usa fallback do variant
  3. Nenhum — variant usa o default brand-aware via Tailwind theme tokens (from-sidebar-button-bg to-sidebar-bg no gradient variant; from-bg-secondary to-bg-primary no 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 mais important: true no tailwind.config.js (removido em 2026-04-29, commit ec8ef0eae, CSS -23.5KB raw). A precedência voltou ao normal: inline style vence utility class. O padrão themeBgClass que você vê em SidebarButtonsGradient.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>.tsx no mesmo folder) + <nome>-registry.ts.
  • Tipos centralizados em app/types/<nome>.ts.
  • Config canônica em app/config/widgets/<nome>.tsmesmo que o componente não seja "widget", o folder widgets/ é 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:

PrimitiveFolderVariantsConfig
GameCardapp/components/games/classic (default), stacked (legado 7k pill)app/config/widgets/game-card.ts
ProviderCardapp/components/home/classic (default), logo-only (legado 7k card wider-than-tall)app/config/widgets/provider-card.ts
GameStats / GameWinnersapp/components/games/sem variants — só campo decoration por brandapp/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 → undefined em 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

PastaOverrideablePropósito
app/config/✅ SimBrand 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ãoMaquinário estrutural dos slots (header, sidebar, footer, banner, right-panel)
app/widgets/❌ NãoComponentes dos widgets opcionais embebidos. Brand controla via app/config/widgets/<nome>.ts
app/router/❌ NãoRegistro de rotas (brands customizam via app/config/routes/paths.ts)
app/types/❌ NãoTipos compartilhados — configs e overrides importam daqui

Os três registrados em sidebarRegistry:

KeyLookQuando usar
sidebar-narrowDefault. Sections "flat" (label + chevron + items abaixo, dividers entre sections)Maioria das marcas
sidebar-accordionSections 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 manualMarcas com look 7k legado (cl-bet7k-com, 7k-bet-br)
sidebar-tabbedSections em abasMarcas 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