Pular para o conteúdo principal

Theming

O sistema de temas usa Tailwind CSS v3 com tokens organizados em objetos aninhados por secao. Forks sobrescrevem os arquivos de app/config/theme/ (substituicao de arquivo inteiro) para personalizar cores, fonte e dimensoes.

app/config/theme/ — o que existe

ArquivoExportPapel
colors.tsthemeColorsPaleta completa: 15 tokens globais + 12 secoes aninhadas
fonts.tsthemeFontsFamilia da fonte + fallback com metricas ajustadas
sizes.tslayoutConfigcontentMaxWidth (→ max-w-content) e dimensoes de logo por area
font-preloads.tsfontPreloadsURLs dos woff2 a precarregar (<link rel="preload">)
header.tsheaderStyleChrome do header: divisoria inferior, classes dos botoes de auth, alinhamento e aparencia da nav
mobile-bottom-nav.tsmobileBottomNavStyleSuperficies da barra de navegacao mobile
search-input.tssearchInputStyleSuperficie visual (borda, fundo, raio, sombra) dos inputs de busca, por surface

Os tres ultimos seguem a mesma divisao de responsabilidade: layout estrutural fica no componente, superficie visual fica no config — assim uma brand troca a aparencia sem forkar o componente.

colors.ts

A estrutura usa objetos aninhados, nao chaves flat com prefixo. A convencao de nome de classe e {utility}-{secao}-{token}:

// app/config/theme/colors.ts
export const themeColors = {
// ── Default (globais) ──
primary: "#a3d712",
"bg-primary": "#212425",
secondary: "#f18825",
"bg-secondary": "#323637",
texts: "#fdffff",
links: "#fdffff",
titles: "#fdffff",
"button-bg": "#a3d712",
"button-text": "#212425",
accent: "#a3d712",
success: "#22bb33",
money: "#beeb0a", // verde "cor de dinheiro" — valores de ganho
error: "#f1416c",
warning: "#ff9f43",
info: "#0096ff",

game: { /* … */ },
header: { /* … */ },
sidebar: { /* … */ },
footer: { /* … */ },
auth: { /* … */ },
payments: { /* … */ },
topbar: { /* … */ },
bottomNotification: { /* … */ },
loader: { /* … */ },
"ftd-cashback": { /* … */ },
"ftd-offer": { /* … */ },
manifest: { /* … */ },
} as const;

export type ThemeColors = typeof themeColors;

Tokens globais

TokenClassesUso
primarybg-primary, text-primaryCor primaria da marca
bg-primarybg-bg-primaryFundo principal do app
secondarytext-secondaryCor secundaria
bg-secondarybg-bg-secondaryFundo secundario
textstext-textsTexto padrao
linkstext-linksLinks
titlestext-titlesTitulos
button-bg / button-textbg-button-bg / text-button-textBotoes globais
accentbg-accent, text-accentDestaque adicional
success / error / warning / infobg-*, text-*Estados
moneytext-moneyValores monetarios de ganho. Mais amarelado que success; cada brand deriva do proprio verde

button-text tambem e exposto como primary-foreground.

:::caution money tem fallback, os outros nao No tailwind.config.js, money: themeColors.money ?? themeColors.success — override que ainda nao propagou o token nao quebra o build. Nenhum outro token tem essa rede. Ver a regra de propagacao mais abaixo. :::

Secoes aninhadas

SecaoPrefixo da classeTokens
game*-game-*title, subtitle, button-bg, button-text, overlay, icon, online-bg, online-text, online-dot, balloon-bg, balloon-text, balloon-value, balloon-icon
header*-header-*bg, links, texts, active-icon, promo-icon, nav-link, nav-link-active, register-bg/text/border, login-bg/text/border, deposit-bg/text/border, balance-bg, balance-text, avatar-bg, top-bg, top-text, main-bg, glow
sidebar*-sidebar-*bg, links, titles, icon, button-bg, button-text, cta-bg, cta-text
footer*-footer-*bg-primary, bg-secondary, links, titles, texts, button-bg, button-text, app-button-bg
auth*-auth-*bg-primary, bg-inputs, text-inputs, bg-close, icon-close, links, titles, texts, button-bg, button-text, backdrop
payments*-payments-*bg-primary, bg-inputs, text-inputs, titles, texts, button-bg, button-text
topbar*-topbar-*bg, text, icon-bg, icon-text, cta-bg, cta-text
bottomNotification*-bottom-notification-*bg, border, text, icon-bg, icon-text, cta-bg, cta-text
loader*-loader-*bar, spinner
ftd-cashback*-ftd-cashback-*first-bg-from/to, first-text-accent, prize-bg-from/to, prize-amount-color, prize-glow-color, prize-coin-color
ftd-offer*-ftd-offer-*Modal + countdown + thumb + template + toast (~26 tokens)
manifesttheme-color, background-color. Nao gera classe Tailwind: e consumido direto por routes/manifest[.]json.ts

:::info Um mapeamento de nome que confunde A chave em colors.ts e bottomNotification (camelCase), mas o nome no Tailwind e bottom-notification (kebab-case) — a traducao acontece no tailwind.config.js. As classes sao bg-bottom-notification-bg, text-bottom-notification-text, etc. :::

Todos os modais de validacao (ValidationBlockerOverlay, ValidationStepsModal, PasswordValidationModal) e o Modal base usam exclusivamente tokens auth.* — trocar essas cores no fork adapta todos os modais de uma vez.

tailwind.config.js

:::danger E .js (ESM), nao .ts, e nao importa o config — ele o PARSEIA O arquivo e tailwind.config.js. Ele nao faz import { themeColors } from "./app/config/theme/…": o Tailwind roda fora do pipeline de aliases do Vite, entao o config le e avalia o arquivo .ts como texto em build time, via readExportFromTs + resolveBrandFile(projectRoot, getBrandKey(), …). O brand key vem de process.env.ORIGIN_DOMAIN ou do ORIGIN_DOMAIN lido do .dev.vars. :::

// tailwind.config.js (trechos reais)
import formsPlugin from "@tailwindcss/forms";
import defaultTheme from "tailwindcss/defaultTheme";
import { resolveBrandFile, toBrandOverrideKey } from "./vite-plugins/brand-resolver.mjs";

const themeColors = readExportFromTs(resolveBrandFile(root, brandKey, "app/config/theme/colors.ts"), "themeColors");
const layoutConfig = readExportFromTs(resolveBrandFile(root, brandKey, "app/config/theme/sizes.ts"), "layoutConfig");
const themeFonts = readExportFromTs(resolveBrandFile(root, brandKey, "app/config/theme/fonts.ts"), "themeFonts");

const sansFontStack = [themeFonts.family, themeFonts.fallback, ...defaultTheme.fontFamily.sans];

export default {
content: ["./app/**/*.{js,jsx,ts,tsx}", "./overrides/**/app/**/*.{js,jsx,ts,tsx}"],
darkMode: "class",
theme: {
colors: {
transparent: "transparent",
current: "currentColor",
white: "#ffffff",
black: "#000000",
primary: themeColors.primary,
// … tokens globais e secoes
},
extend: {
fontFamily: { sans: sansFontStack },
maxWidth: { content: layoutConfig.contentMaxWidth },
screens: { xs: "368px", xxs: "390px" },
// animation, keyframes, fontSize, boxShadow, transitionDuration, gridTemplateRows
},
},
plugins: [formsPlugin, ({ addBase }) => { /* custom properties em :root */ }],
};

O contrato de parse (footgun real)

Porque os arquivos sao parseados, nao importados, eles precisam de uma forma literal exata:

  • export const <nome> = { … } as const; — o regex procura literalmente por as const;
  • Somente valores primitivos. Sem import, sem spread, sem valor computado, sem template string com interpolacao, sem referencia a outra variavel
  • Vale para os tres arquivos parseados: colors.ts, sizes.ts e fonts.ts

Quebrar isso nao produz um erro sutil: o readExportFromTs lanca Could not parse <export> from <file> e o build para.

Paleta

theme.colors substitui a paleta default do Tailwind (nao usa extend), entao text-gray-500, bg-blue-600, bg-zinc-* e afins nao existem.

:::caution bg-white existe, sim theme.colors declara explicitamente transparent, current, white e black. Esses quatro sao a lista de excecoes: bg-white, text-black, bg-transparent, text-current funcionam. O que nao existe e o resto da paleta numerada do Tailwind. :::

Sem important

O config nao tem important: true — a chave foi removida em 2026-04-29 (perf(tailwind): drop important: true — CSS -23.5KB raw (-14.4%)). Qualquer orientacao no sentido de "as classes do Tailwind ganham de style inline" esta invertida hoje. Para vencer especificidade, use ! por classe (!rounded-3xl) — e o padrao adotado nos configs de chrome como headerStyle.

Plugins

PluginSituacao
@tailwindcss/formsO unico plugin de terceiro instalado
addBase inlinePlugin local que injeta custom properties em :root: --auth-text-inputs, --auth-bg-inputs, --color-loader-bar, --font-sans

@tailwindcss/typography, @tailwindcss/aspect-ratio e tailwind-textfill nao estao no package.jsonprose e aspect-w-* nao existem no projeto.

content inclui os overrides

content: ["./app/**/*.{js,jsx,ts,tsx}", "./overrides/**/app/**/*.{js,jsx,ts,tsx}"]

Sem escanear overrides/<brand>/**, uma classe usada apenas por um componente de override seria purgada do build daquela brand e a pagina renderizaria sem estilo.

Custom screens e outros extends

ExtendValor
screens.xs368px
screens.xxs390px
maxWidth.contentlayoutConfig.contentMaxWidth (base: 1400px)
fontSize.2xs0.675rem
fontSize.sm13px / 18px de line-height — o default do Tailwind (14px) foi sobrescrito globalmente, para todas as brands
boxShadow.input0 0 0 100px <auth.bg-inputs> inset (mata o autofill amarelo do Chrome)
transitionDuration.350350ms
animation / keyframesfadeIn, slide-up, arrow-bounce-x, shake, bell-shake, ping-slow

Fonte

A fonte e brand-overridable por arquivo, nao hardcoded no config do Tailwind.

// app/config/theme/fonts.ts
export const themeFonts = {
family: "Montserrat",
fallback: "Montserrat Fallback",
} as const;

O tailwind.config.js monta sansFontStack = [family, fallback, ...defaultTheme.fontFamily.sans] e o usa em dois lugares: theme.extend.fontFamily.sans (utility font-sans) e a custom property --font-sans em :root — que body e os placeholders de input leem no app/tailwind.css. Trocar o arquivo troca a fonte de toda a UI sem tocar em CSS de componente.

Os @font-face / @import ficam em app/tailwind.css, apenas com os subsets latin:

@import "@fontsource/montserrat/latin-400.css";
@import "@fontsource/montserrat/latin-600.css";
@import "@fontsource/montserrat/latin-700.css";

Ha tambem um @font-face de fallback com metricas ajustadas (Montserrat Fallback, local("Arial") com size-adjust / ascent-override / descent-override) — existe para evitar CLS no swap de fonte. O nome da familia declarado em fonts.ts precisa casar com um @font-face real.

Trocar de fonte numa brand exige tres pecas coerentes:

  1. overrides/<brand>/app/config/theme/fonts.ts — nomes das familias
  2. overrides/<brand>/app/config/theme/font-preloads.ts — URLs dos woff2 daquela familia (assim os arquivos de fonte so entram no bundle da brand que os usa)
  3. Os @font-face correspondentes — fontes especificas de brand vivem em app/styles/brand-fonts.css, importado com ?inline e injetado num <style data-brand-fonts> no app/root.tsx (brand-resolved), para que os woff2 de uma fonte so entrem no bundle da brand que a usa

Como personalizar em um fork

  1. Copie o arquivo para overrides/<brand-key>/app/config/theme/<arquivo>.ts
  2. Altere os valores mantendo a estrutura e a forma literal (export const … as const;)
  3. Rode o dev server com o ORIGIN_DOMAIN da brand — o Tailwind recompila com os tokens novos
  4. Para uma secao nova, adicione o objeto aninhado e mapeie a chave em tailwind.config.js (theme.colors) — o mapeamento e explicito, campo por campo

:::danger Override e substituicao de arquivo INTEIRO Nao ha deep-merge. Ao adicionar um token novo em app/config/theme/colors.ts (ou em sizes.ts / fonts.ts), propague para todos os overrides existentes — senao a brand recebe undefined no token e o build quebra ou a cor sai vazia. O money e a unica excecao, com fallback explicito no config. Ver Forking — Override Files. :::