Componentes
1. Botão
Em uma frase: O botão dispara ação e não navega; um único primário por contexto, cinco variantes, seis estados e rótulo verbo+substantivo.
Ficha — Botão
Encurtada do canon; o texto inteiro está na seção ↓ e no Markdown.
Anatomia e partes — ver mais na seção §1.2 ↓
| A · Ícone (opcional) | 16px (sm), 20px (md/lg)… |
|---|---|
| B · Rótulo | Montserrat SemiBold (600)… |
| C · Container | Radius --seed-radius-md (8px)… |
Propriedades e variantes — ver mais na seção §1.3 ↓ · §1.4 ↓
| primary · ALTA | A ação principal da tela/seção… |
|---|---|
| secondary · MÉDIA | Ações relevantes não-principais; par do primário… |
| outline · MÉDIA-BAIXA | Ações neutras/utilitárias… |
| ghost · BAIXA | Ações terciárias… |
| destructive | Ações irreversíveis… |
| link | Ação inline em texto corrido… |
Estados — ver mais na seção §1.5 ↓
| default | action-primary |
|---|---|
| hover | action-primary-hover… |
| active | action-primary-active… |
| focus-visible | + --seed-focus-ring… |
| disabled | surface-sunken + text-disabled… |
| loading | fundo do estado default + spinner… |
Acessibilidade — ver mais na seção §1.8 ↓
<button type="button|submit"> real (nunca div clicável) · foco visível sempre · aria-busy no loading…
Faça · não faça — 3 de 17 decisões, na ordem do canon — ver mais na seção §1.11 ↓ · §1.14 ↓
| faça (decisão) | não faça (descartado) |
|---|---|
md = 44px (não 40px) | 40px + pseudo-elemento de expansão… |
| Loading mantém rótulo + spinner à esquerda | Substituir texto por spinner (padrão v1 informal) |
| Outline mantida como variante própria | Fundir outline+secondary (perderia o caso de uso) |
Relacionados — as 3 mais citadas (de 8):
1.1 Papel#
Dispara uma ação. Não navega entre páginas (isso é link/âncora — pode parecer botão via variante, mas semanticamente <a>). Um grupo de ações tem um único primário; o resto desce na hierarquia.
1.2 Anatomia#
┌───────────────────────────────────────┐
│ [C] container │
│ ┌────┐ ┌─────────────────┐ │
│ │ Íc.│ │ Rótulo │ │
│ └────┘ └─────────────────┘ │
│ (A) (B) │
└───────────────────────────────────────┘
| Parte | Elemento | Regras |
|---|---|---|
| A | Ícone (opcional) | 16px (sm), 20px (md/lg); à esquerda do rótulo por padrão (à direita só para direção: "Avançar →"); gap sp-2 (8px); herda a cor do texto |
| B | Rótulo | Montserrat SemiBold (600); sentence case; nunca some no loading |
| C | Container | Radius --seed-radius-md (8px); padding horizontal por tamanho; borda 1px (transparente exceto outline) |
1.3 Variantes (5 + link)#
| Variante | Quando usar | Fundo / texto (light) |
|---|---|---|
| primary · ALTA | A ação principal da tela/seção — máx. 1 por contexto | FILL SÓLIDO: action-primary (verde institucional, 4.60:1) / text-on-action-primary (v1.44; era text-on-brand, que mede 2,95 no claro — LC-06) |
| secondary · MÉDIA | Ações relevantes não-principais; par do primário | TINT + BORDA DE MARCA: fundo action-secondary-bg (turquesa-50) + borda turquesa-600 (4.60:1 — é ela que identifica o controle) / texto turquesa-800 (8.73:1) |
| outline · MÉDIA-BAIXA | Ações neutras/utilitárias (cancelar, voltar, exportar) | BORDA NEUTRA: transparente + borda cinza-600 (4.74:1) / text-primary |
| ghost · BAIXA | Ações terciárias, barras de ferramenta, densidade (>3 ações no grupo → use ghost) | SÓ TEXTO: transparente / turquesa-700 (6.32:1); hover action-ghost-hover |
| destructive | Ações irreversíveis (excluir, cancelar contrato) | action-destructive / branco |
| link | Ação inline em texto corrido | transparente / text-link, sublinhado no hover; padding zero |
A escada de mecanismos (por que as variantes não se parecem): cada nível de ênfase muda o MECANISMO visual — alta = preenchimento sólido · média = tint + borda de marca · média-baixa = só borda neutra · baixa = só texto. Diferença de mecanismo sobrevive ao aperto de olhos e à escala de cinza; diferença só de tom, não (foi o defeito da v0.1, corrigido aqui).
Regras de grupo (padrão Carbon): 1 alta ênfase por vista; grupos combinam 1 primário + N botões IGUAIS de ênfase menor (nunca primary+secondary+outline misturados no mesmo grupo); com mais de 3 ações, desça para ghost. Nunca destructive como primário "de layout" (ele é vermelho porque destrói, não porque é importante).
1.4 Tamanhos#
| Tamanho | Altura | Padding H | Fonte | Uso |
|---|---|---|---|---|
sm |
32px | 12px | 12px | Interfaces densas desktop (tabelas, toolbars) — não usar como CTA mobile |
full-width (comportamento, v0.7) |
herda lg/md | — | — | Em viewport de toque, CTA único de formulário/fluxo (enviar, confirmar, avançar) estica à largura do container (respeitando safe-areas). Padrão de app/checkout. Nunca para ações secundárias lado a lado |
md (padrão) |
44px | 20px | 14px | Tudo. Já nasce com alvo de toque WCAG |
lg |
52px | 28px | 16px | Hero, CTA de landing, fim de formulário longo |
Icon-only: quadrado (44×44 no md), aria-label obrigatório.
1.5 Estados (os 6 obrigatórios)#
| Estado | Primary (light) | Regra geral |
|---|---|---|
| default | action-primary |
— |
| hover | action-primary-hover (turquesa-700) |
escurece 1 stop; transição 100ms. Por variante (LC-05, 2026-09-15): secondary action-secondary-bg-hover (turquesa-100 · escuro turquesa-800; substitui o filter: brightness(.96), véu que a régua da casa não mede — §71.6) · outline e ghost action-ghost-hover (já era) · destructive action-destructive-hover (já era) |
| active | action-primary-active (turquesa-800 · escuro turquesa-100 desde a v1.29 — V-HV4.2 = a, MANIFESTO §226: era turquesa-400 e o primário escuro clareava no sobrevoo e escurecia no pressionado abaixo da base, a única família não-monotônica do sistema; agora 300 → 200 → 100, tinta #00352F 11,37) |
escurece 2 stops no claro, clareia 2 stops no escuro; sem "pulo" de layout. Por variante (LC-05): secondary action-secondary-bg-active (turquesa-200 · escuro turquesa-700) · outline e ghost action-ghost-active (claro = surface-sunken #E3EBF0, escuro = surface-overlay #273137 — o par de action-ghost-hover, um passo além na mesma direção) · destructive action-destructive-active (vermelho-800 · escuro vermelho-200). Regra: o pressionado é SEMPRE um fundo por token, um passo além do sobrevoo, na mesma direção (§71.2 · §71.6); nunca véu/filter. Contraste da tinta sobre cada fundo novo, medido nos dois temas: 5,24 a 11,37 (claro) · 5,31 a 10,90 (escuro), todos ≥ 4,5 (HV1-c). Descartado com número: action-neutral-active para outline/ghost — no escuro vale #0B1419 = surface-page, e o pressionado ficaria igual ao repouso (pixel: Δ ≤ 1/255); vermelho-500 no destrutivo escuro, 3,38 |
| focus-visible | + --seed-focus-ring |
anel 2px branco + 2px turquesa; visível em QUALQUER variante |
| disabled | surface-sunken + text-disabled |
cursor: not-allowed; sem hover; manter contraste legível do rótulo (o usuário precisa ler o que está indisponível) |
| loading | fundo do estado default + spinner 16px | spinner À ESQUERDA, rótulo permanece (sem layout shift, sem perder contexto); aria-busy="true"; cliques ignorados |
| selected (só em toggles) | fill sólido turquesa-800 + texto/ícone branco + troca de ícone | estado persistente de alternância — ver §1.5.1; aria-pressed="true" |
1.5.1 Botões de alternância (toggle) — sinais de estado (v0.4, alinhada à spec Material 3)#
Botão que liga/desliga algo (mão levantada, filtro ativo, favorito, negrito no editor) tem o estado selected. O estado precisa ser identificável DE RELANCE, no próprio botão, sem interação — texto visível NÃO é requisito (ícone puro é a norma em toolbars).
Sinais visuais no próprio botão (os que resolvem "está ligado?" sem tocar):
- Placa do container: desligado = transparente ou contorno sutil; ligado = placa sólida (turquesa-800, 8.13:1). Mecanismo, não tom.
- Miolo do glifo: desligado = ícone em CONTORNO; ligado = o MESMO ícone PREENCHIDO. Mesma silhueta sempre — a mão continua a mesma mão. Proibido: trocar por ícone diferente, ou usar rasura/corte como indicador de seleção (regra literal do Material 3: outline para não-selecionado, filled para selecionado).
- Forma do container (opcional, recomendado em toolbars com vários toggles): desligado = círculo; ligado = quadrado arredondado (radius-md). Terceiro sinal 100% visual — sobrevive em escala de cinza e para daltônicos (padrão M3 Expressive).
Sinal textual (complementar, nunca o principal): tooltip no hover / long-press no mobile, descrevendo a AÇÃO ("Abaixar a mão") — o visual mostra o estado, o texto diz o que o clique faz. Label visível só quando o layout já é de botão com texto.
Acessibilidade: aria-pressed="true|false" + aria-label sempre (ícone puro sem nome acessível é invisível pra leitor de tela). O aria-label é um nome ESTÁVEL — o mesmo nos dois estados; o estado é só o aria-pressed — e o title fica como tooltip da ação e é lido como descrição (leva 47, HV-P54; guarda guarda-toggle-itgl.mjs cláusula IT1-nome).
Exceção semântica — negação de mídia (microfone, câmera, som): aqui o ícone cortado é CORRETO e esperado, porque a barra comunica o estado da mídia ("som desligado"), não a seleção do botão. Padrão: ativo = glifo normal em placa neutra; desativado = glifo cortado em placa danger sólida (vermelho-600) — dois sinais fortes, convenção universal de apps de chamada. Para estados de constrangimento real (mudo em reunião), somar indicador fora do botão.
Toggle é ferramenta; configuração é Switch. Preferência que persiste (notificações on/off) usa Switch (Bloco 2); botão-toggle é para ações de sessão e ferramentas.
1.6 Tokens de componente (camada 3 — nasce aqui)#
Definidos apenas onde o semântico não basta:
--seed-button-radius: var(--seed-radius-md);
--seed-button-font-weight: var(--seed-fw-semibold);
--seed-button-height-sm: 32px;
--seed-button-height-md: 44px;
--seed-button-height-lg: 52px;
--seed-button-gap: var(--seed-sp-2);
/* containers acessíveis (>=3:1) — a camada 3 existindo pra isso.
REGRA (aprendida na correção v0.5): token de componente que referencia
primitivo DEVE declarar o par dark — primitivo não troca de modo sozinho. */
--seed-button-secondary-border: var(--seed-turquesa-600);
--seed-button-secondary-text: var(--seed-turquesa-800);
--seed-button-outline-border: var(--seed-cinza-600);
--seed-button-ghost-text: var(--seed-turquesa-700);
--seed-button-toggle-on-bg: var(--seed-turquesa-800);
--seed-button-toggle-on-fg: #FFFFFF;
[data-theme="dark"] {
--seed-button-secondary-border: var(--seed-turquesa-300); /* 9.32:1 */
--seed-button-secondary-text: var(--seed-turquesa-100); /* 10.64:1 no tint dark */
--seed-button-outline-border: var(--seed-cinza-400); /* 6.74:1 */
--seed-button-ghost-text: var(--seed-turquesa-300); /* 9.32:1 — era 2.7 com o primitivo fixo */
--seed-button-toggle-on-bg: var(--seed-turquesa-300);
--seed-button-toggle-on-fg: var(--seed-turquesa-900); /* 7.39:1 */
}
Os pares hover/active das cinco variantes NÃO nascem aqui (LC-05, 2026-09-15): são semânticos de AÇÃO da camada 2 (seed-tokens.md §3: action-primary-hover/-active, action-secondary-bg-hover/-active, action-ghost-hover/-active, action-destructive-hover/-active), porque já carregam o par escuro no gêmeo e porque a mesma superfície de estado serve a outros controles da mesma família (chip selecionável, item de lista). Nenhum hex novo: todos são valores da rampa ou da escala de superfície que os gêmeos já tinham.
1.7 Microcopy — regras de rótulo (tom SEED)#
- Verbo + objeto, sentence case: "Solicitar diagnóstico", "Enviar proposta", "Baixar relatório". Nunca "CLIQUE AQUI", nunca "Submeter".
- 1 a 3 palavras. Se precisa de mais, o problema é o contexto, não o botão.
- Destrutivo nomeia a consequência: "Excluir proposta" (não "Confirmar", não "Sim"). Em diálogo de confirmação, o par é "Cancelar" + "Excluir proposta".
- Loading mantém o rótulo — sem trocar para "Aguarde..." (o spinner já diz isso).
- Sem exclamação, sem urgência falsa ("Aproveite JÁ!") — o tom SEED é direto e sincero.
- Exemplos calibrados: ✅ "Ver proposta" · "Falar com engenheiro" · "Calcular economia" | ❌ "Saiba mais!!" · "Clique e descubra" · "OK".
1.8 Acessibilidade#
<button type="button|submit"> real (nunca div clicável) · foco visível sempre · aria-busy no loading · aria-label em icon-only · contraste do rótulo AA em todas as variantes/estados (auditado nos tokens: primary 4.60:1, destructive 5.19:1) · não comunicar estado só por cor (disabled também muda cursor e remove hover; loading tem spinner).
1.9 Código — HTML/CSS (peças, site, e-mails ricos*)#
*em e-mail real, botão é tabela — ver sistema de e-mail (Fase 4).
<button class="seed-btn seed-btn--primary seed-btn--md" type="button">
Solicitar diagnóstico
</button>
<button class="seed-btn seed-btn--primary seed-btn--md" type="button" aria-busy="true" disabled>
<span class="seed-btn__spinner" aria-hidden="true"></span> Enviar proposta
</button>
.seed-btn{
display:inline-flex;align-items:center;justify-content:center;gap:var(--seed-button-gap, var(--seed-sp-2));
font-family:var(--seed-font-sans);font-weight:var(--seed-fw-semibold);
border:1px solid transparent;border-radius:var(--seed-radius-md);cursor:pointer;
transition:background var(--seed-dur-productive-fast) var(--seed-ease-productive),
border-color var(--seed-dur-productive-fast) var(--seed-ease-productive);
}
.seed-btn:focus-visible{outline:none;box-shadow:var(--seed-focus-ring)}
.seed-btn--sm{height:32px;padding:0 12px;font-size:12px}
.seed-btn--md{height:44px;padding:0 20px;font-size:14px}
.seed-btn--lg{height:52px;padding:0 28px;font-size:16px}
.seed-btn--primary{background:var(--seed-action-primary);color:var(--seed-text-on-action-primary)}
.seed-btn--primary:hover:not(:disabled){background:var(--seed-action-primary-hover)}
.seed-btn--primary:active:not(:disabled){background:var(--seed-action-primary-active)}
.seed-btn--secondary{background:var(--seed-action-secondary-bg);
border-color:var(--seed-button-secondary-border);color:var(--seed-button-secondary-text)}
.seed-btn--secondary:hover:not(:disabled){background:var(--seed-action-secondary-bg-hover)}
.seed-btn--secondary:active:not(:disabled){background:var(--seed-action-secondary-bg-active)}
.seed-btn--outline{background:transparent;border-color:var(--seed-button-outline-border);color:var(--seed-text-primary)}
.seed-btn--outline:hover:not(:disabled){background:var(--seed-action-ghost-hover)}
.seed-btn--outline:active:not(:disabled){background:var(--seed-action-ghost-active)}
.seed-btn--ghost{background:transparent;color:var(--seed-button-ghost-text)}
.seed-btn--ghost:hover:not(:disabled){background:var(--seed-action-ghost-hover)}
.seed-btn--ghost:active:not(:disabled){background:var(--seed-action-ghost-active)}
.seed-btn--destructive{background:var(--seed-action-destructive);color:#fff}
.seed-btn--destructive:hover:not(:disabled){background:var(--seed-action-destructive-hover)}
.seed-btn--destructive:active:not(:disabled){background:var(--seed-action-destructive-active)}
.seed-btn:disabled{background:var(--seed-surface-sunken);border-color:transparent;
color:var(--seed-text-disabled);cursor:not-allowed}
.seed-btn__spinner{width:16px;height:16px;border-radius:50%;
border:2px solid currentColor;border-top-color:transparent;
animation:seed-spin .7s linear infinite}
@keyframes seed-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){.seed-btn__spinner{animation-duration:1.4s}}
1.10 Código — React/TypeScript (produtos: SIGA, chat, site Lovable)#
Padrão shadcn/cva; pressupõe os tokens mapeados no Tailwind (guia de integração no Bloco 4).
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md font-semibold " +
"transition-colors duration-100 focus-visible:outline-none " +
"focus-visible:ring-2 focus-visible:ring-[var(--seed-border-focus)] focus-visible:ring-offset-2 " +
"disabled:cursor-not-allowed disabled:bg-[var(--seed-surface-sunken)] disabled:text-[var(--seed-text-disabled)]",
{
variants: {
variant: {
primary:
"bg-[var(--seed-action-primary)] text-[var(--seed-text-on-action-primary)] " +
"hover:bg-[var(--seed-action-primary-hover)] active:bg-[var(--seed-action-primary-active)]",
secondary:
"bg-[var(--seed-action-secondary-bg)] border border-[var(--seed-button-secondary-border)] " +
"text-[var(--seed-button-secondary-text)] hover:bg-[var(--seed-action-secondary-bg-hover)] active:bg-[var(--seed-action-secondary-bg-active)]",
outline:
"border border-[var(--seed-button-outline-border)] text-[var(--seed-text-primary)] " +
"hover:bg-[var(--seed-action-ghost-hover)] active:bg-[var(--seed-action-ghost-active)]",
ghost:
"text-[var(--seed-button-ghost-text)] hover:bg-[var(--seed-action-ghost-hover)] active:bg-[var(--seed-action-ghost-active)]",
destructive:
"bg-[var(--seed-action-destructive)] text-white hover:bg-[var(--seed-action-destructive-hover)] active:bg-[var(--seed-action-destructive-active)]",
link:
"h-auto p-0 text-[var(--seed-text-link)] underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-11 px-5 text-sm",
lg: "h-[52px] px-7 text-base",
icon: "h-11 w-11",
},
},
defaultVariants: { variant: "primary", size: "md" },
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, disabled, children, ...props }, ref) => (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{loading && <Loader2 className="size-4 animate-spin" aria-hidden />}
{children}
</button>
)
);
Button.displayName = "Button";
1.11 Decisões deste bloco (com alternativas descartadas)#
| Decisão | Porquê | Descartado |
|---|---|---|
md = 44px (não 40px) |
Alvo de toque da regra SEED (§0.4) por padrão, sem gambiarras de hit-area — os 44 são convenção da casa, mais rígida que os 24×24 do SC 2.5.8 AA | 40px + pseudo-elemento de expansão (complexidade que alguém esquece) |
| Loading mantém rótulo + spinner à esquerda | Sem layout shift; usuário não perde o contexto do que disparou | Substituir texto por spinner (padrão v1 informal) |
| Outline mantida como variante própria | Útil sobre cards/fotos onde o tonal (turquesa-50) pesa | Fundir outline+secondary (perderia o caso de uso) |
| Disabled com fundo sunken (não opacidade) | Contraste do rótulo continua legível | opacity: .5 (rótulo ilegível, falha WCAG) |
| Link como variante do componente | API única no produto (<Button variant="link">) |
Componente separado (duplicação) |
| Escada de MECANISMOS (v0.2, após crítica do Rafael) | Container de cada variante ≥3:1 (WCAG 1.4.11); v0.1 media 1.07 (secondary) e 2.53 (outline) — variantes indistinguíveis | Diferençar só por tom de tint (reprova em escala de cinza e baixa visão) |
| Tokens de componente com par light/dark obrigatório (v0.5, bug de dark pego pelo Rafael) | No dark, secondary media 1.35:1 e ghost 2.7:1 porque os tokens apontavam pra primitivos fixos | Confiar que "primitivo resolve" (primitivo não troca de modo; só semântico e componente-com-par trocam) |
| Pressionado por TOKEN DE FUNDO em todas as variantes (LC-05, 2026-09-15) | A guarda HV1 media na bancada 55 de 55 .seed-btn com pressionado igual ao sobrevoo (HV1-b) e o secondary com véu filter, não medível (§71.6). Quatro semânticos sem hex novo fecham: action-secondary-bg-hover/-active, action-ghost-active, action-destructive-active; HV1-b da bancada 55 → 0; suíte BT 47/47 |
action-neutral-active em outline/ghost (escuro = página: pressionado igual ao repouso, medido no pixel); vermelho-500 no destrutivo escuro (3,38); manter o filter (contraste não medível) |
| Estado selected com sinais visuais no próprio botão (v0.3→v0.4, dois refinamentos do Rafael) | Toggle é o caso nº1 de ambiguidade documentada (NN/g Mute; Google Meet erra); estado deve ler DE RELANCE sem texto | v0.3 sugeria label visível como sinal e redigia "ícone cortado" ambíguo — corrigido para: tooltip como sinal textual, mesmo glifo contorno↔preenchido (spec M3 literal), rasura só na exceção de negação de mídia |
1.12 Processo de avaliação de hierarquia e qualidade (método — vale para qualquer componente; 7 testes desde a v0.7)#
Aplicar SEMPRE que um componente tiver níveis de ênfase; nasceu da revisão do botão (v0.1→v0.2) e vira padrão dos blocos seguintes:
- Teste do container 3:1 (WCAG 1.4.11): o elemento que identifica o controle (preenchimento OU borda) precisa de contraste ≥3:1 contra a superfície. Medir, não estimar.
- Teste da escada de mecanismos: entre níveis adjacentes, muda o MECANISMO (fill → tint+borda → borda → texto), nunca apenas o tom.
- Teste do aperto de olhos / escala de cinza: renderizar o conjunto em grayscale — os níveis continuam distinguíveis? (Remove a muleta do matiz; simula baixa visão e daltonismo.)
- Teste do par: o par mais comum (alta + média ênfase) lado a lado — a hierarquia é óbvia em 1 segundo, à distância de braço?
- Regra do um: 1 alta ênfase por vista; grupos com 1 primário + N iguais de ênfase menor; >3 ações → ghost.
- Teste do estado atual (toggles): alguém que chega AGORA na tela sabe se está ligado ou desligado sem clicar? Se precisa clicar pra descobrir (o "teste WebEx"), reprovou — aplicar a regra dos 3 sinais (§1.5.1). Método de pesquisa (v0.9, calibrado com o Rafael em 2026-08-02 sobre o histórico de 5 itens): todo item roda NO MÍNIMO 3 rodadas de pesquisa, sempre sequenciais e encadeadas — cada rodada abre declarando as pontas soltas que herda da anterior e busca APENAS o que a base acumulada ainda não responde (decisões já estáveis não se re-pesquisam). Arco típico observado no histórico: rodada 1 = canon dos design systems (~60–70% das decisões); rodada 2 = prática das grandes empresas e do mercado (confirmações + ~20%); rodada 3 = normas e fora-do-circuito (menor volume, maior gravidade — foi onde entraram WCAG 3.3.8/NIST, ISO 4217, APG spinbutton, WCAG 2.5.7). Rodada extra (4ª+) por gatilho, nunca por rito: crítica do Rafael expondo lacuna (caso 0800/EUA do form-field), decisões instáveis após 3 rodadas, ou domínio de alto risco (dinheiro, dados pessoais, segurança). Uma rodada que não herda pergunta aberta é redundante e não deve acontecer.
§1.13 — Protocolo de live region SEED (v0.12, transversal): todo anúncio dinâmico a leitores de tela segue UM padrão (nasceu na busca D4, confirmado no textarea F3, agora governa combobox, upload e switch async): região role="status" (polite) + aria-atomic="true", visualmente oculta e separada do nó visual (canais independentes — os 4 bugs do GOV.UK vieram de misturar); anúncio com debounce ~1s (fala na pausa, não a cada evento); frase completa e específica ("14 resultados para 'inversor'", "proposta.pdf enviado"), nunca fragmento; erro que exige ação imediata usa role="alert"; uma live region por componente. Consumidores: §4, §6, §8 (pending), §11, §14.
Nota 1.13-b (v0.15 — emenda, dois regimes de anúncio): o §1.13 nasceu para atualizações contínuas (contagem de busca a cada tecla), onde anunciar cada evento defasaria o visual — daí os canais separados + debounce (D4-b). Mensagens discretas (toast, alerta injetado, banner dinâmico) são conteúdo novo inserido UMA vez: não há defasagem possível, e o padrão universal correto (Spectrum, React Aria, Radix, Base Web, gov.br) é o próprio nó visível ser a live region, injetado já populado. Regra de implementação que a suite cobra nos dois regimes: o container com role precisa existir no DOM ANTES do conteúdo ser inserido — inserir container e conteúdo juntos faz leitores de tela perderem o anúncio (WCAG 4.1.3, técnica documentada). Sem esta nota, a validação executada do Bloco 3 reprovaria o padrão correto por leitura literal do §1.13. Descartado: exigir canal separado também para mensagens discretas (duplicaria o DOM sem ganho e contraria todo o mercado verificado).
- Teste do polegar (v0.7 — mobile): renderizar o componente em viewport de 360px (Android BR mais comum) com teclado virtual aberto (altura útil ~450px): todos os alvos ≥44px ou com hit-area estendida documentada; nada essencial coberto pelo teclado; ordem de leitura íntegra em coluna única; texto do campo ≥16px (anti-zoom iOS); zoom 200% não quebra (WCAG 1.4.4). O preview traz a faixa 360px pré-renderizada, como a faixa grayscale.
O preview traz a faixa em escala de cinza pré-renderizada para o teste 3 ser feito a olho.
1.14 Botão flutuante (FAB) — decidido (2026-10-10) · FB1–FB8 · LC-18 · pedido PC-2026-0008#
Contexto. FAB = botão flutuante de ação (floating action button). Pedido pelo SEED Chat (
PC-2026-0008, 2026-09-13; aprovado na triagem, MANIFESTO §235.1) e estudado no G9-12 (2026-09-24,09-pesquisa/2026-09-24-grupo-9/lc-abas-fab/). É uma variante de COMPORTAMENTO do botão primário (como ofull-widthdo §1.4), não um componente novo. Veredito dele, verbatim (colado no chat em 2026-10-10 ~01:47Z, junto com a barra de abas do §95): "V-AB1 a, V-AB2 b, V-FB1 c, V-FB2 b, veto/observação: nenhum" — aqui valem V-FB1 c (estendido, ícone + nome da ação, 56 de altura) e V-FB2 b (ao descer esconde o nome e fica só o "+"; ao subir ou no topo o nome volta; nunca some).
| # | Decisão | Porquê (medido ou citado) | Descartado |
|---|---|---|---|
| FB1 | É a forma ESTREITA do botão primário da vista, para uma ação CONSTRUTIVA (criar). Um por tela; enquanto ele está na tela, não há outro primário (regra do um, §1.12-5). Acima do limiar do §95 AB3 (860 px de contêiner app), a mesma ação volta a ser o botão primário md no cabeçalho do conteúdo. Nunca destrutivo, nunca navegação, nunca ação secundária |
HIG Buttons (16/12/2025): "Keep the number of prominent buttons to one or two per view". Medido no estudo: 1 primário visível em 8 de 8 capturas da família | FAB para qualquer ação · FAB e botão primário no topo ao mesmo tempo |
| FB2 (V-FB1 c) | Forma = ESTENDIDO: ícone + rótulo verbo+objeto (§1.7), 56 de altura; padding 0 sp-5 0 sp-4, rótulo 14 px peso 600, ícone 24 |
Material 3 (tokens androidx, lidos em 2026-09-24): FAB 56 dp, forma CornerLarge = 16 dp, ícone 24. HIG: "prefer circular or capsule-shape buttons". O DS já decidiu que ícone sozinho não é o padrão (SH3); o estendido é o único que diz o que faz |
56 × 56 raio 16 só ícone (V-FB1 a, o que o SEED Chat usa hoje) · 56 × 56 círculo (b) · 80 × 80 (d) · sem FAB (e) |
| FB3 | Medidas: altura/lado 56 = sp-12 + 2 × sp-1 (a mesma composição do rail recolhido, SH19 — 56 não é passo da escala, é soma de passos); raio radius-xl (16); recolhido (FB7), 56 × 56 |
O "56 px" do pedido tem fonte: é o FabBaselineTokens do Material 3 (56 dp, 16 dp); o SEED Chat usa exatamente isso (h-14 w-14 rounded-seed-8) |
inventar número fora da escala |
| FB4 | Cor e material: action-primary + tinta text-on-action-primary + receita metal_acao (MA2: metal na ação primária) + shadow-overlay (ele flutua). Foco: anel do §1.5 |
Tinta medida por token: 4,60 (claro) · 7,39 (escuro). A receita metal_acao troca a sombra pela borda do metal; o FAB soma as duas |
sombra só do metal (não lê como flutuante) · cor própria de FAB |
| FB5 | Posição: canto inferior direito da peça de conteúdo, sp-4 das bordas dela (= 28 px da tela com a calha sp-3 do CP1), acima da barra de abas; z-index 60 (faixa de chrome: abaixo de modal 65, toast 70, popover 75) |
Material 3: margem de 16 dp. Medido: borda direita 28, folga até a barra 28 (= calha 12 + sp-4 16), FAB ∩ barra = 0 em 7 de 7. Deslocar o toast para cima do FAB é [inferência, não renderizado] |
16 px da borda da TELA (sairia da peça de conteúdo e encostaria na calha) |
| FB6 | Reserva: o conteúdo que rola reserva no fim altura do FAB + 2 × sp-4 (padding-bottom + scroll-padding-bottom), derivada do tamanho do botão — uma fonte só |
Medido: com a reserva FIXA de 56 + 32, o FAB de 80 px cobria 580 px² da última linha rolada até o fim; com a reserva derivada, 0 em 7 de 7. É o princípio do SL7 e a técnica do WCAG 2.4.11 (scroll padding) | reserva constante |
| FB7 (V-FB2 b) | Ao rolar: ao DESCER, o estendido recolhe para 56 × 56 (só o "+"); ao SUBIR ou no topo, estende de novo. Nunca some. Muda só quando a direção muda (histerese de ±2 px); prefers-reduced-motion tira a transição |
Material 3 (padrão do FAB estendido). Medido: com a troca a cada evento, o evento nativo seguinte (mesmo scrollTop) desfazia o recolher; com a histerese, recolhe (56 × 56 a 244 de 488 px rolados). HIG: esconder a navegação faz a pessoa esquecer onde está — o mesmo vale para a ação |
nada muda (V-FB2 a) · sumir ao descer e voltar ao subir (c) |
| FB8 | Acessibilidade: <button> real; nome = o rótulo visível (estendido) ou aria-label (recolhido/só ícone); recolhido, o rótulo sai da vista e FICA NO DOM (clip de 1 px, como o rail recolhido do SH19), nunca display:none. Na ordem do DOM, logo após o cabeçalho do conteúdo (onde o botão primário vive no desktop) |
Medido (prova-nome do estudo): com display:none o botão recolhido ficou sem nome na AX (1 botão sem nome); com o clip, "Nova proposta" (7 de 7 capturas com nome). A posição no DOM é [não medida]: a camada da prancha o anexa depois do conteúdo |
display:none no rótulo · title como nome |
Ainda não é guarda do rito e não há bancada: a emissão (classe/atributo no controles_bloco.py ou no módulo do shell) e a prova nascem depois — pendência a nascer. O estudo (camada.css, camada.js, captura.mjs) é a base.
Também cita o §1: banco-cn, banco-feedback, tela-autenticacao, tela-cadastro-indicado, tela-detalhe, tela-envio-fornecedor, tela-estado, tela-shell-abas.