DESIGN-SKILL.md
A linguagem de design do Open Analytics em um único arquivo, escrita para agentes de IA e para os humanos que os orientam. Cada valor aqui é enviado no produto: códigos hexadecimais, constantes de mola, raios, atenuações. Cole este arquivo no contexto do seu agente (ou coloque-o no seu repositório como um arquivo de regras) e peça telas "no estilo OA".
Esta é a edição compilada em um único arquivo, feita para colar. A mesma linguagem
também está disponível neste repositório como uma skill de agente adequada
(skills/oa-design/: receitas por componente com o código-fonte completo verificado por tipo
embutido) e como uma CLI (npx getopen-design add <componente>); prefira estas
para o Claude Code e outros agentes com suporte a skills.
A aparência em uma frase: superfícies brancas com cantos de curvatura contínua, repousando sobre um palco cinza silencioso, desenhadas em uma única tinta, movidas por uma única mola.
1. As dez regras
- Uma tinta, tudo derivado. Todo o sistema neutro é uma cor misturada em transparência em percentagens fixas. Nunca escolha um segundo cinza.
- Duas camadas, e o espaço é a página. As superfícies são um quadro branco segurando uma inserção cinza rebaixada; as seções são placas e o fundo da página entre elas é o único divisor. Sem linhas horizontais.
- Squircle para superfícies, pílulas para ações. Cards têm cantos de curvatura contínua; tudo que é clicável que não é um card é uma pílula.
- Uma família de molas. Sete molas nomeadas cobrem todo o produto. Não invente uma oitava.
- O Chrome nunca espera. Layout e títulos renderizam instantaneamente; apenas as trocas de dados vêm de um esqueleto com pixels correspondentes, chegando por desfoque, não por pop.
- O peso para em 500. Sem negrito em nenhum lugar. A hierarquia vem do tamanho, cor e espaçamento.
- Um acento, gasto em um só lugar. Um único azul para ações primárias e a linha primária do gráfico; as cores semânticas são matizes de texto, não preenchimentos.
- Estados recebem pílulas, eventos terminam a si mesmos. Condições permanentes renderizam faixas não dispensáveis que vivem exatamente enquanto o estado dura; resultados únicos são toasts que se aposentam sozinhos.
- O texto é parte do design. Letra inicial maiúscula em frases; botões dizem o que acontece; erros nomeiam a causa e a saída, sem culpa.
- Piso de qualidade, sempre. Anéis de foco,
role="status", decoraçõesaria-hidden,prefers-reduced-motione sem rolagem horizontal na página.
2. Tokens
Claro
:root {
--ink: #292929;
--background: #f6f6f6; /* o palco cinza entre as placas */
--card: #ffffff;
--popover: #ffffff;
/* neutros derivados da tinta: as percentagens SÃO o sistema */
--border: color-mix(in srgb, var(--ink) 12%, transparent);
--input: color-mix(in srgb, var(--ink) 14%, transparent);
--accent: color-mix(in srgb, var(--ink) 5%, transparent); /* lavagem hover */
--muted: color-mix(in srgb, var(--ink) 5%, transparent);
--muted-foreground: #6d6d6d;
--foreground: var(--ink);
--primary: #305dde; /* o único acento */
--primary-foreground: #ffffff;
--ring: #3ba6f1;
--secondary: #e9e9e9; /* controles cinza planos, sem borda */
--secondary-foreground: var(--ink);
/* semântico, como matizes de TEXTO em claro (escala Tailwind, linha 700) */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-red-700);
--success-foreground: var(--color-emerald-700);
--warning-foreground: var(--color-amber-700);
--info-foreground: var(--color-blue-700);
--chart-1: #296FF0; /* linha primária do gráfico */
--radius: 0.625rem;
}
Precisa de um divisor mais forte? Aumente a percentagem de tinta (o produto usa apenas 4, 5, 10, 12, 14). As cores semânticas vão em palavras e pontos de 8px, nunca em preenchimentos de painel.
Escuro
Mesma receita, retintada, com duas mudanças sistemáticas: as percentagens de linha fina sobem em 2 pontos, e o texto semântico passa da linha 700 para a linha 400.
.dark {
--ink: #ededed;
--background: #191919;
--card: #212121;
--popover: #242424;
--border: color-mix(in srgb, var(--ink) 14%, transparent);
--input: color-mix(in srgb, var(--ink) 16%, transparent);
--muted-foreground: #969696;
--secondary: #2e2e2e;
--primary: #296FF0;
--destructive-foreground: var(--color-red-400);
--success-foreground: var(--color-emerald-400);
}
Raio
Uma base, multiplicada; nunca um raio personalizado:
--radius: 0.625rem; /* sm ×0.6, md ×0.8, lg ×1, xl ×1.4,
2xl ×1.8, 3xl ×2.2, 4xl ×2.6 */
rounded-full é reservado para pílulas. Superfícies squircle têm seus próprios raios
maiores (seção 4).
Tipografia
- Inter Tight, variável, pesos 300 a 500, auto-hospedada (CDNs de fonte quebram builds; mantenha o woff2 no repositório).
- Geist Mono para
code, kbd, samp, pre, teclas e trechos. - O teto de peso é a identidade: nada renderiza mais pesado que 500.
A hierarquia vem do tamanho,
text-foreground/80vstext-muted-foreground, e espaçamento. - Cabeçalhos:
font-medium tracking-tight; títulos de marketing adicionamtext-wrap: balance. - Dados:
tabular-numsem qualquer número em uma coluna ou atualizando no lugar. - A voz padrão da UI é 14px; linhas secundárias 12px, com
leading-5/leading-6.
Sombras
A elevação é binária:
/* card em repouso */ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
/* chrome flutuante */ box-shadow: 0 1px 2px rgba(0,0,0,0.08),
0 8px 24px rgba(0,0,0,0.08);
Disciplina de fundo
body é branco (o overscroll mostra o body; cinza aparecendo atrás de uma tela
branca parece uma página diferente). Telas que querem o palco cinza pintam
seu próprio fundo. A raiz recebe overflow-x: clip.
3. Layout
Placas. As seções são painéis autocontidos; o fundo da página aparecendo
entre elas é o único limite. Espaços verticais generosos
(gap-12 sm:gap-20 marketing, gap-6 in-app), padding lateral mais apertado
(px-4 sm:px-6). Irmãos são espaçados pelo gap do pai, nunca por
margens por filho. Sem <hr>, sem border-b entre seções.
Larguras por escopo: marketing 76rem (6.5xl), páginas do site do app max-w-6xl,
telas de lista/seletor max-w-5xl, superfícies de leitura ~65ch. Centralizado, mx-auto.
Anatomia da página do app:
cabeçalho fixo (h-14) → container pt-14
pílula flutuante opcional (fixed top-16, centralizado)
<main class="px-4 pb-36 pt-8 sm:px-6 bg-white">
linha de título: h1 text-xl font-medium tracking-tight + UM controle à direita
faixa(s) de aviso opcional(is)
placas: flex flex-col gap-6
A linha de título contém exatamente um controle. pb-36 mantém o conteúdo longe da
borda da viewport.
Grades: grid gap-3, colunas responsivas (grid-cols-2 sm:grid-cols-4 para
estatísticas). Cards compartilham uma altura de conteúdo fixa e rolam internamente; a linha de base
da grade nunca quebra.
Responsivo: design em 390px e 1280px. Pares horizontais empilham em
sm: com a ação por último. Conteúdo largo rola em seu próprio
contêiner overflow-x-auto; a página nunca rola lateralmente.
Configurações: trilho de abas estreito + coluna de conteúdo com painéis de seção;
cada seção abre com um bloco de cabeçalho (título + uma frase cinza, ação única opcional
à direita). Abas com link profundo via ?tab=<id>.
4. Componentes
A superfície squircle
Cantos de curvatura contínua via um clip-path shape() CSS parametrizado por
duas variáveis, com corner-shape: squircle onde houver suporte:
.squircle {
border-radius: 26px;
--card-clip-radius: 14px;
--card-clip-handle: 2.25px;
clip-path: shape(
from var(--card-clip-radius) 0px,
line to calc(100% - var(--card-clip-radius)) 0px,
curve to 100% var(--card-clip-radius)
with calc(100% - var(--card-clip-handle)) 0px / 100% var(--card-clip-handle),
line to 100% calc(100% - var(--card-clip-radius)),
curve to calc(100% - var(--card-clip-radius)) 100%
with 100% calc(100% - var(--card-clip-handle)) / calc(100% - var(--card-clip-handle)) 100%,
line to var(--card-clip-radius) 100%,
curve to 0px calc(100% - var(--card-clip-radius))
with var(--card-clip-handle) 100% / 0px calc(100% - var(--card-clip-handle)),
line to 0px var(--card-clip-radius),
curve to var(--card-clip-radius) 0px
with 0px var(--card-clip-handle) / var(--card-clip-handle) 0px,
close
);
corner-shape: squircle;
}
@media (min-width: 640px) {
.squircle { border-radius: 50px; --card-clip-radius: 20px; --card-clip-handle: 3px; }
}
O mini card
squircle externo: bg-card, border, p-1, sombra de repouso
├── faixa de cabeçalho (pb-2 pl-3.5 pr-2 pt-1.5):
│ ícone muted de 16px + título text-sm font-medium text-foreground/80
│ chip opcional na linha central do título
│ "Ver tudo ›" (text-xs muted; hover: lavagem accent, seta desliza 2px)
└── squircle interno: bg #f6f6f6, border, rounded-[22px] sm:rounded-[44px],
altura FIXA (h-44 = cinco linhas de 32px), overflow hidden
Cards nunca crescem com dados; listas mais longas rolam dentro. Overflow mostra um
chip de chevron de 24px no canto inferior direito, balançando y:[0,2.5,0] (loop 1.4s easeInOut), desaparecendo
(0.18s, escala 0.7) no final da rolagem. Linhas: 32px, lavagem accent de sangria total
no hover, valores tabular-nums.
Botões
Pílulas, font-medium, pressionados fisicamente:
active:translate-y-px active:scale-[0.98].
- primary: azul de acento com um bisel (background/border color-mix em direção a um
índigo profundo,
inset 0 1px 0 rgba(255,255,255,0.22)topo, inset escuro fundo); hover clareia para primary puro. - secondary:
--secondaryplano, borda transparente, hover mistura 5% de tinta. O burro de carga. - ghost: apenas texto muted. destructive: preenchimento vermelho, apenas etapa de confirmação.
- Tamanhos h-9 / h-8 / h-7; o chrome do app usa principalmente os pequenos.
- Carregando mantém o rótulo e adiciona um spinner de anel de 14px
(
border-2 border-current border-t-transparent); largura nunca pula. focus-visible:ring-[3px]em--ring/50. Nunca removido.
Painéis de dropdown (menus, seletores, chaveadores)
Gatilho pílula → painel flutuante (sombra flutuante, borda, branco, rounded-2xl+), entrada com mola em PANEL (550/38) com pequeno deslocamento y/scale, saída mais rápida. Itens rounded-lg de 32px com lavagem accent no hover; o destaque ativo é um elemento compartilhado que viaja (layoutId ou molas de width/height), nunca repaints por item. Fecha em pointerdown fora e Escape.
Modais
Backdrop desbota ~0,15s; painel entra com mola em POP (400/26) a partir de
opacity 0, scale .96, sai em 380/28. Diálogos de várias etapas mantêm um painel
cuja altura anima para o tamanho medido de cada etapa (LAYOUT 550/40 +
ResizeObserver) enquanto as etapas deslizam (x: ±12px, popLayout, 0,16s
easeOut fades). Rodapé: faixa h-12, Back (secondary xs) + primary xs,
alinhado à direita.
Barra de abas
A pílula ativa é uma janela fixa; os rótulos deslizam atrás dela. O rótulo que chega entra da direção de viagem a 110% de sua própria largura; o que sai sai a 130px fixos no outro sentido (uma percentagem falha quando a pílula cresce em direção a uma palavra mais longa). Largura/altura da pílula seguem o rótulo medido em molas LAYOUT. Opacidade total durante todo o tempo; a máscara faz o trabalho.
Esqueletos
- O Chrome nunca espera; apenas a área de dados é esqueleto.
- Correspondência de pixels: uma barra fica dentro da mesma linha de altura que o texto que
representa (
flex h-6 items-centerenvolvendo uma barrah-3); a troca não move nada. - Os dados chegam por foco: uma animação única de
opacity ~0.4 + blur(4px)para nítido. Uma animação, não uma transição (o conteúdo monta já final; uma transição não tem valor anterior). Mantenhafilter: blur(0)depois.
Barra: rounded bg-muted-foreground/15, varredura lenta de posição de fundo
(2s linear infinito, deslocamento inicial -1s para que as barras fiquem fora de fase).
Avisos, estados vazios, a pílula flutuante
Faixa de aviso acima do conteúdo que explica:
<div class="flex flex-col gap-2 rounded-2xl border border-border bg-card
px-4 py-3 sm:flex-row sm:items-center sm:justify-between"
role="status">
<p class="text-sm leading-6 text-muted-foreground">
<span class="font-medium text-foreground">Nenhum evento ainda.</span>
Se o snippet estiver ativo, navegue pelo seu site e pegaremos a primeira
pageview em segundos.
</p>
<button class="…secondary pill…">Verificar a instalação</button>
</div>
Uma afirmação em negrito, uma frase cinza, uma ação secundária; empilha em sm:;
vive exatamente enquanto o estado dura; sem dispensar.
Pílula flutuante para condições permanentes em todo o app: fixed top-16, centralizada,
rounded-full bg-card py-1.5 pl-3 pr-4 text-xs font-medium, sombra flutuante,
ponto de status de 8px (pulsando quando urgente), entrada com mola em BANNER (400/30) a partir de
y:-8, saída 0.15s. A pílula inteira linka para onde o estado é resolvido.
Pílulas de estado nunca se dispensam automaticamente; pílulas de evento se aposentam após ~6s.
Toasts
Abaixo de 0,35s, atenuação cubic-bezier(0.5, 1, 0.89, 1):
- Sucesso: pulso
scale 1 → 1.025 → 0.99 → 1(0,32s). - Erro: tremor
x 0 → -3 → 3 → -3 → 0px (0,28s).
Cópias de keyframe ímpares/pares alternadas para que as repetições recomecem.
5. Movimento
| Nome | Valor | Usado para |
|---|---|---|
| PANEL | 550 / 38 | dropdowns, menus, painéis, alternadores |
| LAYOUT | 550 / 40 | altura/largura medida, pílulas deslizantes |
| POP | 400 / 26 | entrada de modal |
| POP_EXIT | 380 / 28 | saída de modal |
| BANNER | 400 / 30 | pílulas flutuantes, banners de página |
| FLICK | 900 / 50 | micromovimentos de ícone |
| CHART | 300 / 25-30 | tooltips e crosshair de gráfico |
Micro fades: 0,1s a 0,18s easeOut. Nada no chrome do app faz tween por mais de 0,2s; se parecer lento, diminua o amortecimento, não aumente a duração.
Movimentos característicos: coreografia de altura medida (um painel constante, etapas deslizando), o destaque viajante (um elemento ativo compartilhado, nunca repaints por item), assimetria de entrada/saída (saídas sempre mais rápidas), hover como apenas cor (mais no máximo um deslize de seta de 2px; cards nunca levantam ou escalam), e no máximo um loop ambiente minúsculo por tela.
Regras: AnimatePresence para tudo que desmonta; respeite
prefers-reduced-motion (revelações colapsam para visível, loops param); nunca
anime layout não medido; nunca leia o relógio de parede durante a renderização (inscreva-se
em uma store de ticks em intervalos de minuto).
6. Páginas de destino
Mesmos tokens, s
…
[truncado para tradução]