name: antigravity-design-system description: "Use esta skill sempre que uma tarefa envolver qualquer aspecto visual do projeto Gravity — cores, tipografia, espaçamentos, sombras, bordas, componentes visuais ou tokens de design. Define o tema Solid Slate (Material 3 Adaptation), todas as variáveis CSS obrigatórias, escala tipográfica com Plus Jakarta Sans, sistema de botões pill, campos de formulário, badges, toasts, KPI cards, kanban, tabs, popover de filtro, select customizado, wizard timeline, modais complexos e modal de permissões. Todo agente consulta esta skill antes de escrever qualquer CSS ou componente visual."
Gravity — Design System
Tema: Solid Slate (Material 3 Adaptation)
Dark mode é o padrão. Light theme é ativado via body.light-theme.
1 — Variáveis Globais (Root CSS)
:root {
/* === Backgrounds === */
--bg-body-dark: #0f172a;
--bg-base: #1e293b;
--bg-surface: #334155;
--bg-elevated: #475569;
/* === Acento principal (Indigo 500) === */
--accent: #6366f1;
--accent-hover: #4f46e5;
/* === Texto === */
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
/* === Status === */
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
/* === Geometria === */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-pill: 9999px;
/* === Focus ring === */
--focus-ring: 0 0 0 2px rgba(99, 102, 241, 0.4);
/* === Sombras === */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4);
--shadow-md: 0 4px 12px rgba(0,0,0,0.5);
}
body.light-theme {
--bg-body-dark: #f8fafc;
--bg-base: #ffffff;
--bg-surface: #f1f5f9;
--bg-elevated: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
}
2 — Escala Tipográfica
Fontes: Plus Jakarta Sans (UI) e DM Mono (código). Carregar via Google Fonts.
/* Tipografia — Plus Jakarta Sans obrigatório */
body { font-family: 'Plus Jakarta Sans', sans-serif; }
code, pre { font-family: 'DM Mono', monospace; }
.text-display { font-size: 2.25rem; font-weight: 700; line-height: 1.2; }
.text-h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.3; }
.text-h2 { font-size: 1.5rem; font-weight: 600; line-height: 1.3; }
.text-h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.4; }
.text-body-lg { font-size: 1rem; font-weight: 400; line-height: 1.6; }
.text-body { font-size: 0.875rem; font-weight: 400; line-height: 1.6; }
.text-sm { font-size: 0.8125rem;font-weight: 400; line-height: 1.5; }
.text-micro { font-size: 0.75rem; font-weight: 600; line-height: 1.4; letter-spacing: 0.06em; text-transform: uppercase; }
Regra:
.text-microé sempre UPPERCASE. Nunca use para corpo de texto corrido.
3 — Sistema de Botões (Pill System)
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1.25rem;
border-radius: var(--radius-pill); /* OBRIGATÓRIO — sempre pill */
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
border: none;
}
.btn-primary {
background: var(--accent);
color: #0f172a;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary {
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--bg-elevated);
}
.btn-secondary:hover { background: var(--bg-elevated); }
.btn-ghost {
background: transparent;
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--bg-surface);
color: var(--text-primary);
}
.btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
4 — Campos de Formulário
.input-group {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.input-group label {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-secondary);
}
.input-group input,
.input-group textarea,
.input-group select {
background: var(--bg-base);
border: 1px solid var(--bg-elevated);
border-radius: var(--radius-md);
padding: 0.5rem 0.75rem;
color: var(--text-primary);
font-size: 0.875rem;
}
.input-group input:focus,
.input-group textarea:focus {
outline: none;
box-shadow: var(--focus-ring);
border-color: var(--accent);
}
Placeholder — Spec Obrigatória
Todo campo vazio exibe um placeholder (texto de referência). A spec abaixo é inviolável — vale para <input>, <textarea>, <select>, SelectGlobal (.sg-placeholder), CampoDecimalGlobal e qualquer componente de formulário.
/* Placeholder canônico — Gravity Design System */
input::placeholder,
textarea::placeholder,
.sg-placeholder {
font-family: 'Plus Jakarta Sans', sans-serif; /* fonte UI — NUNCA monospace */
font-size: 0.875rem; /* 14px — .text-body */
font-weight: 400; /* normal */
font-style: normal; /* NUNCA italic */
color: var(--text-muted, #64748b); /* dark: #64748b · light: #94a3b8 */
opacity: 1; /* browsers aplicam 0.54 por default — forçar 1 */
text-align: left; /* mesmo em campos numéricos right-aligned */
}
| Propriedade | Valor | Justificativa |
|---|---|---|
font-family |
'Plus Jakarta Sans', sans-serif |
Fonte UI padrão — DM Mono é só para código/valores numéricos preenchidos |
font-size |
0.875rem (14px) |
Classe .text-body — alinha com valor selecionado (.sg-valor-selecionado) |
font-weight |
400 |
Normal — valores preenchidos usam 500, placeholder fica mais leve |
color |
var(--text-muted) |
Dark #64748b / Light #94a3b8 — token de texto terciário |
opacity |
1 |
Explícito — Firefox/Chrome reduzem para 0.54 se não forçado |
text-align |
left |
Placeholder é texto descritivo, sempre à esquerda — valor numérico muda para right ao digitar |
Regras de conteúdo do placeholder
- Nunca usar valor válido do domínio como placeholder — ex: proibido
placeholder="USD"em campo de moeda (confunde com seleção real) - Texto descritivo curto — ex:
"Selecionar moeda","Quantidade","Valor unitário" - Campos computados/read-only — usar
"Calculado"(indica que o sistema preenche) - Formato de exemplo — permitido quando inequívoco:
"0000.00.00"para NCM,"SKU"para part number - i18n obrigatório — todo placeholder deve usar
t()(ver skilltraducao)
Override em CampoDecimalGlobal
O CampoDecimalGlobal aplica fontFamily: monospace e textAlign: right via inline style. Como ::placeholder herda do input, é obrigatório fazer override via CSS scoped:
.meu-form input::placeholder {
font-family: 'Plus Jakarta Sans', sans-serif !important;
text-align: left !important;
color: var(--text-muted, #64748b) !important;
opacity: 1 !important;
}
O !important é justificado: inline styles têm especificidade 1000, e ::placeholder herda deles. Sem !important, a fonte e alinhamento do placeholder ficam errados.
Hints e Dicas Abaixo de Campos (3 tiers oficiais)
Texto auxiliar abaixo de campos segue 3 tiers com propósitos distintos. Cada tier tem spec visual própria — nunca misturar.
| Tier | Nome | Propósito | Ícone? | Tamanho | Cor |
|---|---|---|---|---|---|
| 1 — Hint padrão | .cg-hint |
Orientação estática (ex: "Formato: 0000.00.00") | ❌ Sem ícone | 0.8rem |
var(--text-muted, #94a3b8) |
| 2 — Dica contextual | .cg-hint-contextual |
Sugestão/informação dinâmica (ex: "Sugestão automática — você pode editar") | ✅ Info 14px fill |
0.8rem |
var(--text-muted, #94a3b8) |
| 3 — Status badge | Custom | Estado de validação (ex: "NCM válido", "Verificando...") | ✅ Semântico 12px | 0.75rem |
Semântica (verde/amarelo/vermelho) |
Tier 1 — Hint Padrão (via CampoGeralGlobal)
Texto puro, sem ícone. Usa a prop hint do CampoGeralGlobal:
<CampoGeralGlobal label="NCM" hint="Formato: 0000.00.00">
<input ... />
</CampoGeralGlobal>
CSS canônico (em campo-geral.css):
.cg-hint {
display: block;
font-size: 0.8rem;
color: var(--ws-muted, var(--text-muted, #94a3b8));
}
Regra: hint desaparece quando há
erro— o erro tem prioridade visual.
Tier 2 — Dica Contextual (com ícone Info)
Para quando o campo tem comportamento que o usuário precisa entender — sugestão automática, valor calculado editável, preenchimento inteligente. O ícone Info sinaliza "isso é uma informação sobre o campo, não um erro".
import { Info } from '@phosphor-icons/react'
<CampoGeralGlobal label="Número do Pedido" obrigatorio>
<input ... />
</CampoGeralGlobal>
<span className="cg-hint-contextual">
<Info size={14} weight="fill" style={{ flexShrink: 0, opacity: 0.6 }} />
Sugestão automática — você pode editar livremente.
</span>
Spec visual:
.cg-hint-contextual {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8rem;
color: var(--text-muted, #94a3b8);
}
| Propriedade | Valor | Justificativa |
|---|---|---|
| Ícone | <Info size={14} weight="fill"> |
Phosphor Info, fill para destaque sutil |
| Opacidade do ícone | 0.6 |
Suficiente para ser notado sem competir com o label |
gap |
0.375rem (6px) |
Espaçamento confortável entre ícone e texto |
font-size |
0.8rem |
Alinhado com .cg-hint (Tier 1) |
color |
var(--text-muted, #94a3b8) |
Mesmo tom do hint padrão |
Quando usar Tier 2 vs Tier 1: se o texto descreve formato ou restrição → Tier 1 (hint puro). Se descreve comportamento do campo (sugestão, cálculo, preenchimento automático) → Tier 2 (com ícone).
Tier 3 — Status Badge
Para indicar estado de validação dinâmico — campo sendo verificado, válido, inválido. Cada estado tem cor e ícone semânticos.
// Exemplo: validação de NCM
<span className="ncm-status ncm-status--valido">
<CheckCircle size={12} weight="fill" />
NCM válido
</span>
| Estado | Cor | Ícone |
|---|---|---|
| Válido | var(--success) (#22c55e) |
CheckCircle 12px fill |
| Atenção | var(--warning) (#f59e0b) |
Warning 12px fill |
| Erro | var(--danger) (#ef4444) |
XCircle 12px fill |
| Carregando | var(--text-muted) (#94a3b8) |
ArrowsClockwise 12px + animação spin |
Anti-pattern proibido: usar Tier 3 (badge colorido) para informação estática. Badge é só para estado que muda.
Padrão de Campo Obrigatório (3 sinais oficiais)
Quando um campo é obrigatório e está vazio, o sistema mostra 3 sinais redundantes com propósitos distintos (alinhado com Material Design / Apple HIG):
| Sinal | Tipo | Onde | Propósito |
|---|---|---|---|
Asterisco * vermelho |
estático | ao lado do label | "este campo é obrigatório, sempre será" |
| Borda vermelha no input | dinâmico | volta do <input> / <select> |
"está vazio AGORA — precisa preencher" |
Banner BannerRequisitosGlobal |
consolidado | acima do botão de avançar/salvar | lista TUDO que falta com mensagens amigáveis |
Anti-pattern proibido: legenda "* Campos obrigatórios" no rodapé do form. Asterisco já tem semântica universal; legenda é ruído visual redundante.
Componente canônico
import { CampoGeralGlobal } from '@nucleo/campo-geral-global'
<CampoGeralGlobal
label="Número do Pedido"
obrigatorio
vazio={!form.numero_pedido.trim()} // <- regra: vermelho dispara quando obrigatorio && vazio
erro={erros.numero_pedido} // <- opcional: mensagem de validação pós-submit
>
<input value={form.numero_pedido} onChange={...} />
</CampoGeralGlobal>
Path: nucleo-global/Campos/campo-geral-global/
CSS interno (regra de cores e prioridade)
A classe .cg-wrapper--erro é aplicada automaticamente quando (obrigatorio && vazio) || erro. O CSS abaixo (em campo-geral.css) pinta de vermelho:
.cg-wrapper--erro .sg-campo,
.cg-wrapper--erro input,
.cg-wrapper--erro select,
.cg-wrapper--erro textarea {
border-color: #f87171 !important;
}
Sobre o !important: justificado e obrigatório. Muitos consumidores aplicam border inline via style={{...}} (especificidade 1000). Sem !important, a borda vermelha nunca aparece pra inputs com border inline. Estado de erro é exceção que ganha prioridade — espelha aria-invalid no plano visual.
Componentes custom (que não usam CampoGeralGlobal diretamente)
Quando precisar de layout especial (ex: label com botão "+ Nova" inline), siga o mesmo padrão manualmente:
- Adicione
className="cg-wrapper cg-wrapper--erro"no wrapper quandovazio || erro— reaproveita a CSS canônica - Renderize o asterisco vermelho com
<span style={{ color: '#f87171', marginLeft: '0.125rem' }}>*</span> - Use
BannerRequisitosGlobalno nível do formulário pra listar pendências consolidadas
Nunca use outline: 1px solid red ou hack visual próprio — fica invisível ou inconsistente.
5 — Badges de Status
.badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.6rem;
border-radius: var(--radius-pill);
font-size: 0.75rem;
font-weight: 600;
}
.badge-success { background: rgba(34,197,94,0.15); color: var(--success); }
.badge-warning { background: rgba(245,158,11,0.15); color: var(--warning); }
.badge-danger { background: rgba(239,68,68,0.15); color: var(--danger); }
6 — Toasts (Notificações do Sistema)
Regra: nunca criar elementos de toast manualmente. Usar sempre
addNotificationvia Shell.
const { addNotification } = useShellStore()
addNotification({ type: 'success', message: 'Operação concluída com sucesso' })
addNotification({ type: 'error', message: 'Erro ao processar requisição' })
addNotification({ type: 'warning', message: 'Atenção: campo obrigatório' })
7 — KPI Cards (Dashboard)
.kpi-card {
background: var(--bg-surface);
border-radius: var(--radius-lg);
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.kpi-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.kpi-value {
font-size: 1.875rem;
font-weight: 700;
color: var(--text-primary);
}
8 — Ícones
Biblioteca obrigatória: @phosphor-icons/react
import { House, Users, Buildings } from '@phosphor-icons/react'
Peso padrão
| Contexto | Peso (weight) |
|---|---|
| UI geral (nav, header, botões) | "duotone" |
| Destaque / call-to-action | "fill" |
| Ícone sutil / placeholder | "regular" |
Regra: sempre usar
weight="duotone"como padrão, salvo necessidade explícita.
Tamanhos canônicos
| Contexto | size |
|---|---|
| Sidebar nav item | 18 |
| Header título (inline com texto) | 20 |
| Botão com texto | 16 |
| Botão ícone-only / toggle | 18 |
| Badge | 14 |
Cor
- Ícones de acento (destaque, header):
color="#6366f1"ou#818cf8(=--accent) - Ícones neutros (nav, botões secundários): herdam
colordo elemento pai via CSS
Ícones Reservados por Contexto (Regras de Uso)
| Elemento | Ícone obrigatório | Proibido |
|---|---|---|
| Botão Hub no header | Graph |
ArrowLeft, CaretLeft, ArrowBack, qualquer seta |
Regra: o botão "Hub" em qualquer header/topbar usa
<Graph>— nunca seta. O íconeGraphrepresenta "rede/cluster/hub"; setas representam "voltar", o que cria confusão UX. Esta regra foi estabelecida após regressões repetidas em múltiplos layouts (Core, Workspace, Admin). A fonte de verdade éservicos-global/configurador/src/components/HubButton.tsx.
Espaçamento (gap)
O ícone nunca tem margin próprio — o espaçamento é controlado pelo flex container pai:
| Contexto | gap |
|---|---|
Botão (.btn) |
0.5rem |
Nav item (.ws-nav-item) |
0.75rem |
Header título (.ws-header__title-row) |
0.5rem |
Badge (.badge) |
0.25rem |
/* Exemplo: título de header com ícone */
.ws-header__title-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
// Exemplo de uso em header
<div className="ws-header__title-row">
<House weight="duotone" size={20} color="#6366f1" />
<p className="ws-header__title">Área do Cliente</p>
</div>
9 — Navigation Tabs
/* Pill Tabs — para seções de página */
.tabs-pill { display: flex; gap: 0.25rem; padding: 0.25rem; background: var(--bg-surface); border-radius: var(--radius-pill); }
.tab-pill { padding: 0.375rem 1rem; border-radius: var(--radius-pill); font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); cursor: pointer; transition: all 0.15s; }
.tab-pill.active { background: var(--bg-base); color: var(--text-primary); box-shadow: var(--shadow-sm); }
/* Underline Tabs — para conteúdo aninhado em modais */
.tabs-underline { display: flex; gap: 0; border-bottom: 1px solid var(--bg-elevated); }
.tab-underline { padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s; }
.tab-underline.active { color: var(--accent); border-bottom-color: var(--accent); }
10 — Sistema de Cores por Zona e Produto
Referência completa:
documentos-tecnicos/UX/cores.html
Dois Tiers de Cor
| Tier | Quem | Cor | Lógica |
|---|---|---|---|
| 1 — Plataforma | HUB, Core, Admin | #818cf8 (Gravity Indigo) |
Unidade institucional |
| 1 — Plataforma | Configurador | #f472b6 (Pink 400) |
Workspace admin — diferente dos produtos |
| 2 — Produto | 8 produtos COMEX | Cor individual | Identidade única por produto |
Cores por Produto (Fonte: @nucleo/logo-produtos)
import { getProdutoMeta } from '@nucleo/logo-produtos'
// getProdutoMeta(productId).color → cor de acento
// SimulaCusto → '#34d399' Emerald 400
// Pedido → '#f59e0b' Amber 400
// BID Câmbio → '#06b6d4' Cyan 500
// BID Frete → '#60a5fa' Blue 400
// LPCO → '#f43f5e' Rose 500
// NF Importação → '#c084fc' Purple 400
// Processo → '#facc15' Yellow 400
// Financeiro COMEX → '#f472b6' Pink 400
Os 3 Pontos de Contato (ÚNICA aplicação da cor de produto)
| Ponto | Onde | CSS |
|---|---|---|
| ① Chip do produto | Topbar — lado esquerdo | background: linear-gradient(135deg, {color}18, {color}08) |
| ② Dot da sidebar | Item ativo — indicador | background: {color} — circle 6×6px |
| ③ Fundo item ativo | Menu lateral — selecionado | background: {color}12; color: {color} |
Regras Invioláveis
- Cor de acento via prop — nunca hardcoded em componente. Vem de
getProdutoMeta(productId).color - Máximo 3 pontos de contato — nunca em fundos de página, botões, titles, badges de status
- KPI cards — único card que aceita cor:
border-top: 2px solid {color} - Botões primários — sempre
var(--accent)(#6366f1 indigo), nunca cor do produto - Fonte única de verdade —
nucleo-global/Logo/produtos/src/produtos.tsx(PRODUTO_META)
11 — Select Box Customizada
Regra: nunca usar
<select>nativo do HTML. Usar sempre o componenteadvanced-select.
import { CaixaSelectGlobal } from '@nucleo/caixa-campo-select-global'
12 — Wizard Timeline (Stepper)
.stepper { display: flex; align-items: flex-start; gap: 0; }
.step { display: flex; flex-direction: column; align-items: center; flex: 1; }
.step-circle {
width: 2rem;
height: 2rem;
min-width: 2rem; /* OBRIGATÓRIO — sem isso o círculo deforma */
flex-shrink: 0; /* OBRIGATÓRIO — sem isso o círculo encolhe */
border-radius: 50%;
background: var(--bg-elevated);
color: var(--text-muted);
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.step-circle.active { background: var(--accent); color: #0f172a; }
.step-circle.done { background: var(--success); color: white; }
.step-connector { flex: 1; height: 2px; background: var(--bg-elevated); margin-top: 1rem; }
.step-connector.done { background: var(--success); }
14 — Layout Profundo de Modal
/* Header do Modal — sempre --bg-surface */
.modal-header {
background: var(--bg-surface);
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--bg-elevated);
}
/* Body do Modal — sempre --bg-base */
.modal-body {
background: var(--bg-base);
padding: 1.5rem;
flex: 1;
overflow-y: auto;
}
/* Footer do Modal */
.modal-footer {
background: var(--bg-surface);
padding: 1rem 1.5rem;
border-top: 1px solid var(--bg-elevated);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
15 — Modal de Permissões (Grid de Checkboxes)
.permissions-grid {
display: grid;
grid-template-columns: 1fr repeat(4, auto);
gap: 0.5rem 1.5rem;
align-items: center;
}
.permission-module-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
}
.permission-header {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
text-align: center;
}
Regras de Uso Obrigatórias
- Sem cores hardcoded — sempre usar variáveis CSS
- Sem
<select>nativo — usaradvanced-select/CaixaSelectGlobal - Botões sempre pill —
border-radius: var(--radius-pill)obrigatório - Modais complexos: header e footer em
--bg-surface, body em--bg-base - Ícones: exclusivamente Phosphor Icons
- Tipografia: exclusivamente Plus Jakarta Sans (UI) e DM Mono (código)
.text-micro: sempre uppercase; nunca para corpo de texto corrido- Steppers:
min-widtheflex-shrink: 0obrigatórios nos círculos - Toasts: usar
addNotificationvia Shell; nunca criar elementos manualmente - Tema: dark é o padrão; light theme via
body.light-theme - Placeholders:
'Plus Jakarta Sans',0.875rem,var(--text-muted),opacity: 1,text-align: left— mesmo em campos numéricos. Nunca monospace, nunca valor válido do domínio como texto de placeholder