Ir ao conteúdo
SEED engenharia

Componentes

1. Botão

estávelseed-componentes.md v2.00 · §01seção 4 de 18401-botao-estavel.md · MD5 07169106

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 ↓

Anatomia e partes — Botão: Parte · Elemento · Regras
A · Ícone (opcional)16px (sm), 20px (md/lg)…
B · RótuloMontserrat SemiBold (600)…
C · ContainerRadius --seed-radius-md (8px)…

Propriedades e variantes — ver mais na seção §1.3 ↓ · §1.4 ↓

Propriedades e variantes — Botão: Variante · Quando usar · Fundo / texto (light)
primary · ALTAA ação principal da tela/seção…
secondary · MÉDIAAções relevantes não-principais; par do primário…
outline · MÉDIA-BAIXAAções neutras/utilitárias…
ghost · BAIXAAções terciárias…
destructiveAções irreversíveis…
linkAção inline em texto corrido…

Estados — ver mais na seção §1.5 ↓

Estados — Botão: Estado · Primary (light) · Regra geral
defaultaction-primary
hoveraction-primary-hover…
activeaction-primary-active…
focus-visible+ --seed-focus-ring…
disabledsurface-sunken + text-disabled…
loadingfundo 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 à esquerdaSubstituir texto por spinner (padrão v1 informal)
Outline mantida como variante própriaFundir outline+secondary (perderia o caso de uso)

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)
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):

  1. Placa do container: desligado = transparente ou contorno sutil; ligado = placa sólida (turquesa-800, 8.13:1). Mecanismo, não tom.
  2. 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).
  3. 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)#

  1. Verbo + objeto, sentence case: "Solicitar diagnóstico", "Enviar proposta", "Baixar relatório". Nunca "CLIQUE AQUI", nunca "Submeter".
  2. 1 a 3 palavras. Se precisa de mais, o problema é o contexto, não o botão.
  3. Destrutivo nomeia a consequência: "Excluir proposta" (não "Confirmar", não "Sim"). Em diálogo de confirmação, o par é "Cancelar" + "Excluir proposta".
  4. Loading mantém o rótulo — sem trocar para "Aguarde..." (o spinner já diz isso).
  5. Sem exclamação, sem urgência falsa ("Aproveite JÁ!") — o tom SEED é direto e sincero.
  6. 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:

  1. 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.
  2. Teste da escada de mecanismos: entre níveis adjacentes, muda o MECANISMO (fill → tint+borda → borda → texto), nunca apenas o tom.
  3. 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.)
  4. 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?
  5. Regra do um: 1 alta ênfase por vista; grupos com 1 primário + N iguais de ênfase menor; >3 ações → ghost.
  6. 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).

  1. 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 o full-width do §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.


BancadaSEED — Componentes — Bloco 1: Botãoabrir em página própria ↗

Também cita o §1: banco-cn, banco-feedback, tela-autenticacao, tela-cadastro-indicado, tela-detalhe, tela-envio-fornecedor, tela-estado, tela-shell-abas.

Esc