HeatMapXHeatMapX
PreçosEntrar

Reconstruindo um Dashboard SaaS em Um Dia com Claude Code — Tailwind v4 + Alinhamento Tabular com CSS Grid

HeatMapX Engineering Team14 min read
  • engineering
  • tailwind
  • css-grid
  • dark-mode
  • claude-code

TL;DR

  • Problema: 11 de 15 novos usuários (73%) fizeram o cadastro mas saíram sem registrar nenhuma URL
  • Hipótese: Todas as ferramentas de heatmap da nossa categoria usam UI clara por padrão — nosso dashboard escuro pode estar causando confusão e abandono precoce
  • O que fizemos: Redesign completo do dashboard em um dia — toggle de tema claro / escuro / sistema + lista tabular no estilo GitHub
  • Lições: @custom-variant do Tailwind v4, a armadilha da coluna auto no CSS Grid, uma defesa em 3 camadas contra textos longos e por que 10 a 16 iterações de design é o normal, não a exceção
  • Resultado: Impacto na taxa de abandono ainda a ser medido; post de acompanhamento planejado após duas semanas de dados

Contexto: Estávamos Perdendo Usuários Por Causa do Dashboard Escuro Demais?

HeatMapX (heatmapx.com) é uma ferramenta de heatmap e CRO que pode ser chamada a partir do Claude Code via CLI. Depois de iniciar nosso primeiro esforço de divulgação, percebemos um padrão: 11 de 15 novos usuários (73%) completaram o cadastro mas saíram sem registrar nenhuma URL.

Uma hipótese: toda ferramenta de heatmap relevante na nossa categoria vem com uma UI clara por padrão. O HeatMapX era o único com interface de administração escura. Usuários chegando pela primeira vez a um dashboard escuro em um mar de apps com tema claro podem estar simplesmente saindo por confusão.

Então fizemos parceria com o Claude Code e realizamos um redesign completo em uma única sessão. Este post cobre a primeira metade desse teste de hipótese — diagnosticar o abandono e executar o redesign. O acompanhamento com os números reais virá assim que tivermos duas semanas de dados pós-deploy.

Configuração Final do Design

Elemento Claro Escuro
Fundo da página bg-neutral-100 (#f5f5f5) dark:bg-neutral-950 (#0a0a0a)
Superfície do card bg-white + border-slate-200 dark:bg-slate-900 + dark:border-slate-800
Texto principal text-slate-900 dark:text-slate-100
Cor principal (CTA) bg-orange-600 (#ea580c) Compartilhada entre os dois modos
Raio de borda rounded-md (6px) · sem sombras · estilo GitHub
Toggle de tema ☀ / 💻 / 🌙 toggle de três estados no cabeçalho

Dark Mode com Tailwind v4 — Do Zero-Config ao Toggle Manual

Começando com @media prefers-color-scheme

O comportamento padrão do Tailwind v4 mapeia a variante dark: diretamente para @media (prefers-color-scheme: dark). Sem nenhuma configuração, ele simplesmente acompanha a configuração de modo escuro do sistema operacional.

{/* layout.tsx */}
<div className="bg-neutral-100 text-slate-900 dark:bg-neutral-950 dark:text-slate-100">
  ...
</div>

Isso cobre o caso de seguir o sistema (escuro automático à noite, etc.) sem nenhuma configuração adicional. Mas às vezes os usuários querem substituir a configuração do SO — "sistema" pode falhar, ou é simplesmente preferência pessoal — então estendemos para um toggle de três estados.

Mudando para a Variante Dark Baseada em Classe

No Tailwind v4 você pode redefinir o comportamento da variante dark: usando @custom-variant. Mudamos para um modo onde dark: só é ativado quando .dark está presente em <html>:

/* globals.css */
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

O wrapper :where() mantém a especificidade em 0. Sem ele, os estilos da variante dark podem entrar em conflito com regras de especificidade existentes em outras partes da sua folha de estilos.

Script de Sincronização para Evitar Flash na SSR

Se você aplica o tema dentro de useEffect após a hidratação do React, você obtém um flash de conteúdo sem estilo (FOUC) — um piscar branco breve no primeiro render. A solução é um script de sincronização inline injetado no <head> que é executado antes do body renderizar:

{/* app/layout.tsx */}
<head>
  <script dangerouslySetInnerHTML={{ __html: `
    (function(){try{
      var t = localStorage.getItem('theme') || 'system';
      var d = t === 'dark' || (t === 'system' &&
        window.matchMedia('(prefers-color-scheme: dark)').matches);
      if (d) document.documentElement.classList.add('dark');
    }catch(e){}})();
  `}} />
</head>

Isso é executado sincronamente antes do React montar, então .dark já está no elemento <html> quando o body começa a renderizar. Adicione suppressHydrationWarning em <html> para silenciar o aviso de incompatibilidade de hidratação.

💡 Lição 1: O dark: do Tailwind v4 funciona sem configuração. Se rastrear o sistema operacional é tudo que você precisa, não precisa tocar em nada. Só adicione @custom-variant + o script de sincronização quando precisar especificamente de um toggle manual pelo usuário — uma abordagem limpa em dois estágios.

Alinhamento Tabular com CSS Grid — e Onde Colunas auto Te Pegam

A Lista Inicial de Uma Coluna Não Era Suficiente

Depois de notar que os cards de site pós-registro pareciam "estranhos", primeiro tentamos o óbvio: trocamos um grid de cards de duas colunas por uma lista de uma coluna. Cada SiteCard usava flexbox internamente para alinhar nome · url · status · eventos, com um chevron à direita.

O problema apareceu assim que havia múltiplos itens:

Curto · example.com · ● Ativo · 5.577
Nome de Site Muito Longo... · normal.com · ● Ativo · 5.577
ACME · acme.io · ● Ativo · 5.577

O · Ativo, · 5.577 e o › chevron caem em coordenadas x diferentes em cada linha. Seu olho não consegue fixar em "a coluna de status é aqui." Não parece uma lista — parece strings empilhadas.

Aquilo não era uma lista. Era texto empilhado verticalmente.

Convertendo para um Layout Tabular com CSS Grid

Substituímos o flexbox interno por uma definição de coluna fixa com CSS Grid. Como todos os cards compartilham o mesmo template de grid, as bordas esquerdas das colunas se alinham perfeitamente ao longo da página:

// SiteCard.tsx
<Link className="grid grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1.5fr)_minmax(0,3fr)_110px_72px_20px] items-center gap-x-4 ...">
  <h3 className="truncate">{name}</h3>
  <span className="truncate">{displayUrl}</span>
  <span className="truncate font-mono">{apiKey}</span>
  <span className="truncate font-mono">{trackerSnippet}</span>
  <span>● Ativo</span>
  <span>{count}</span>
  <span>›</span>
</Link>

A linha de cabeçalho usa o exato mesmo template de grid:

// page.tsx linha de cabeçalho
<div className="grid grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1.5fr)_minmax(0,3fr)_110px_72px_20px] gap-x-4 ...">
  <span>SITE</span>
  <span>URL</span>
  <span>API KEY</span>
  <span>TRACKER TAG</span>
  <span>STATUS</span>
  <span>HOJE</span>
  <span></span>
</div>

A Armadilha: Colunas auto em Grids Separados São Dimensionadas Independentemente

Nossa primeira versão usava grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto_auto_auto]. O raciocínio: "auto se adapta ao conteúdo, e como o cabeçalho e o SiteCard compartilham a mesma string de template, eles vão se alinhar."

Não alinharam. Eis o motivo:

  • A coluna auto do cabeçalho: dimensionada com a largura do texto "STATUS"
  • A coluna auto do SiteCard: dimensionada com a largura do texto "● Ativo"
  • Ambos são containers de grid separados — o dimensionamento das trilhas é calculado independentemente para cada um

Duas soluções:

  1. Largura fixa: Substitua auto por valores explícitos em pixels como 110px, 72px (o que colocamos em produção)
  2. CSS Subgrid: Coloque o cabeçalho e todos os SiteCards dentro de um único grid pai, então faça cada linha herdar as trilhas do pai via subgrid (mais complexo)

💡 Lição 2: Não use colunas auto quando precisa de alinhamento tabular entre containers de grid separados. Cada container calcula suas próprias larguras de trilha baseado em seu próprio conteúdo. Use larguras fixas em pixels ou compartilhe trilhas via subgrid.

Defendendo-se Contra Textos Longos — 3 Camadas

O schema inicial do banco de dados tinha:

CREATE TABLE sites (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,  -- sem limite de tamanho
  url text NOT NULL,   -- sem limite de tamanho
  ...
);

O text do Postgres é efetivamente ilimitado. Um nome de site com 10.000 caracteres é SQL perfeitamente válido — e destruiria um layout tabular instantaneamente. Migrar para um grid de colunas fixas tornou as restrições de tamanho de texto inegociáveis.

A defesa em 3 camadas que chegamos:

  1. Limite HTML5 no frontend: maxLength={60} / maxLength={512} nos inputs
  2. Validação no servidor: Server Action verifica name.length > 60 e retorna um erro
  3. Truncamento na exibição: Classe truncate em cada célula do grid, combinada com minmax(0, ...) para habilitar o overflow

A camada 3 é a que as pessoas esquecem. truncate se expande para overflow: hidden + text-overflow: ellipsis + white-space: nowrap, mas em um contexto flex ou grid também precisa de min-width: 0 na célula — caso contrário, a célula se expande para caber seu conteúdo e o overflow nunca é acionado. minmax(0, ...) fornece exatamente isso.

16 Iterações de Design

Só o SiteCard passou por 16 iterações de design nesta sessão. Algumas selecionadas:

# Mudança Motivo
1 Converteu CopyBlock para pílula cinza clara Fundo preto + texto verde ficou estranho no modo claro do GitHub
3 Comparação de quatro opções A/B/C/D Precisava de affordance de clique mais forte
4 Mudou para lista de 1 coluna Rejeitada como "decorativa demais" — de volta ao básico
7 Removeu CTA "Ver Heatmap →" "Não parece limpo"
9 Converteu para layout tabular com CSS Grid Layout de string com separador "não parece uma lista"
10 auto → larguras fixas em pixels Colunas do cabeçalho e do card não alinhavam
11 Restaurou coluna API KEY "Espera, onde está a chave de API?"
12 Adicionou coluna TRACKER TAG "Coloque os dois lado a lado"
14 Adicionou maxLength + validação no servidor Correção adequada para robustez com textos longos
16 Adicionou botão "+" ao lado do título (modo compacto) Adição rápida sem precisar rolar

Não espere acertar na primeira tentativa. Iteração de design significa "verificar no servidor de desenvolvimento → feedback imediato → próxima tentativa", repetida 10+ vezes. Planeje esse ciclo. Um ambiente de desenvolvimento de IA interativo como o Claude Code é onde esse padrão realmente brilha.

Padrões Que Realmente Funcionaram com Desenvolvimento Assistido por IA

Essas não são observações específicas de ferramentas — são padrões gerais que se mostraram eficazes ao trabalhar em par com uma IA interativa:

  • Zero custo de busca para nova sintaxe de API: O @custom-variant do Tailwind v4 é relativamente novo. O Claude Code o trouxe imediatamente em forma funcional, sem precisar vasculhar documentação.
  • O ciclo implementar → verificar → corrigir roda em segundos: Reproduzir o desalinhamento auto do CSS Grid, isolar a causa e publicar a correção levou cerca de 5 minutos no total.
  • Trabalho mecânico em massa desaparece: Substituir texto de placeholder em 15 arquivos de localização (in-golexample.com) em uma única iteração.
  • Afinidade com HMR: Editar → salvar → atualização instantânea no browser → feedback → reeditar. O ciclo roda rápido o suficiente para você permanecer em estado de fluxo.
  • Do início ao fim em uma sessão: Do término do redesign direto até o push em produção, assistindo ao deploy no Vercel — sem troca de contexto.

Os padrões que não funcionaram tão bem estão na próxima seção.

O Que Foi Difícil / O Que Ainda Resta

  • Traduzir feedback vago em decisões estruturais multiplicou o número de iterações. "Isso está estranho" precisava se tornar "tabular vs. lista vs. cards" antes que algo útil pudesse acontecer. Chegar a esse acordo estrutural logo no início teria economizado 3 a 4 iterações.
  • Por volta da iteração 10, os requisitos continuavam se expandindo — "tudo inline", "tudo truncado", "todas as colunas" — e algumas restrições estavam genuinamente em tensão (densidade de informação vs. facilidade de leitura). Não havia atalho limpo para a forma final.
  • Mobile não foi tocado. Um grid de 7 colunas produz rolagem horizontal em telas pequenas. A correção é ou uma media query sm: que empilha colunas, ou um componente mobile separado. Esta é a próxima coisa a atacar.

Conclusão

Em uma única sessão, reconstruímos o dashboard do HeatMapX de um grid de cards escuro para um layout com toggle claro/escuro/sistema e uma lista tabular no estilo GitHub. As principais conclusões técnicas:

  1. Dark mode com Tailwind v4: Comece com dark mode baseado em @media — é zero-config. Só adicione @custom-variant + o script de sincronização quando os usuários precisarem de um toggle manual.
  2. Alinhamento tabular com CSS Grid: Colunas auto não se alinham em containers de grid separados. Use larguras fixas em pixels ou CSS Subgrid — essas são as únicas opções confiáveis.
  3. Robustez com textos longos: Três camadas, todas necessárias — maxLength no frontend, verificação de tamanho no servidor e truncate na exibição com minmax(0, ...).
  4. Orçamento de iterações de design: Assuma 10 a 16 iterações por componente e construa um ciclo de feedback rápido desde o início.

Se isso realmente vai mover a taxa de abandono ainda é uma questão em aberto. Publicaremos o acompanhamento com duas semanas de dados reais.

Heatmaps no Claude Code — comece grátis.

Cole uma tag de tracking e receba análises e sugestões CRO via CLI.