Componentes
68. Editor de texto rico — toolbar mínima
seed-componentes.md v1.43 · §68seção 74 de 9868-editor-de-texto-rico-toolbar-minima-estavel-f7.md · MD5 e4f9203aTítulo completo no canon: Editor de texto rico — toolbar mínima — estável (F7.5, 2026-08-16 · promovido em 2026-08-17 pelo gate visual do Rafael — verbatim: "aprovado") · ET1–ET14 · fecha C7 · bancada banco-texto-rico.html v0.2 · suite-texto-rico.mjs 25 · 0 · 0
O que é, e o que explicitamente NÃO é. Este §68 é o editor de CAMPO: a descrição de uma ordem de serviço, a observação de um laudo, o corpo de um comentário. Toolbar mínima, marcas fechadas, registro tipográfico de interface.
Ele não é o editor de DOCUMENTO (arquétipo A13), e essa fronteira não é preferência — ela está medida. O dispositivo C25 do
estudo-clickup-completo.mdregistra, no editor de documento do produto de referência: coluna de 660px, texto 16px / line-height 24px (=1,5) e uma stack de fonte diferente da do resto do aplicativo. Interface quer 14px/1,15 e densidade; leitura corrida quer 16px/1,5 e medida de linha controlada — usar o mesmo registro nos dois piora os dois. Um componente só para os dois casos não serviria bem a nenhum. A13 continua AUSENTE do mapa de cobertura, e é assim que fica dito.
68.0 Rodada zero — leitura visual MEDIDA, e ela decidiu a fronteira#
Veredito: MEDIDO — e é o caso raro em que a leitura visual entrou na spec decidindo o que fica
de fora. Os dispositivos lidos: C25 (registro tipográfico próprio da coluna de leitura),
C101 (o documento abre como overlay com ×, não como rota), C103 (rail de ferramentas
vertical flutuante à direita), C105 (toolbar flutuante de bloco, que aparece ao passar o
mouse), C106 (callout como bloco tingido).
Todos os cinco pertencem ao editor de DOCUMENTO. Nenhum deles é do editor de campo — e é por isso que eles aparecem aqui como fronteira, não como contrato: importar a toolbar flutuante de bloco (C105) para um campo de descrição de OS traria um controle que só aparece com o mouse por cima, sobre um campo de três linhas. Leitura visual bem lida também serve para dizer o que não se copia.
68.1 RITO — três rodadas encadeadas#
| Rodada | Fonte | O que trouxe |
|---|---|---|
| R1 — canon | ARIA APG — Toolbar Pattern · SC 2.1.2 No Keyboard Trap | A toolbar é composite: um único tab stop, setas navegando dentro, Home/End nas pontas (ET2). E o 2.1.2 é o que decide o Tab (ET6): editor que insere tabulação prende quem navega por teclado |
| R2 — mercado/stack | PatternFly · Semrush Intergalactic · prática de editores (ProseMirror/Lexical/TipTap) | Do mercado vem a confirmação de que o estado do botão precisa seguir o cursor, não o último clique (ET3), e a lição estrutural: o modelo do documento é da biblioteca de edição; o contrato é do DS. Daí a fronteira ET-P1 |
| R3 — normas | WCAG 2.2: SC 1.3.1 Info and Relationships · SC 4.1.2 Name, Role, Value · SC 4.1.3 Status Messages | O 1.3.1 é o que transforma "usar <strong> e não <b>" de gosto em norma: a marcação tem de carregar a relação, não só a aparência. O 4.1.2 exige role/nome/valor na área editável (ET4) e nos alternadores (ET3). O 4.1.3 põe o contador em região de status (ET13) |
68.2 A decisão estruturante: o conjunto de marcas é FECHADO em seis#
Negrito · itálico · lista com marcadores · lista numerada · link · limpar formatação.
O motivo não é estético, é de custo em quatro lugares. O valor deste campo não fica na tela:
ele é reimpresso na proposta (seed-ds-proposta), no e-mail (seed-email.md) e no PDF.
Toda marca que o editor aceita é uma marca que os outros três meios precisam saber desenhar — e
o e-mail é o mais restrito dos quatro. Uma marca nova custa quatro implementações e quatro testes,
não um.
Descartado, com o porquê: títulos e tabelas dentro do campo (são estrutura de documento, e o documento é o A13) · cor de texto e realce (não invertem no tema escuro, e cor não é informação que sobrevive à reimpressão) · imagem embutida (vira anexo, que é o §14) · Markdown como formato de entrada (exigiria que a pessoa soubesse a sintaxe — o oposto de toolbar mínima).
68.3 Os contratos ET1–ET14#
| # | Contrato | Por quê | Descartado |
|---|---|---|---|
| ET1 | Conjunto de marcas fechado em seis | Ver 68.2. O gerador aborta acima de oito — acima disso a toolbar deixa de ser mínima e o custo deixa de ser visível | Toolbar “completa” estilo processador de texto (quatro implementações por marca) |
| ET2 | A toolbar é role="toolbar", nomeada, com aria-controls e um tab stop; setas andam dentro |
APG. Seis botões na ordem de tabulação obrigariam seis Tab para chegar ao texto |
Botões soltos (seis paradas antes do campo) |
| ET3 | Cada alternador tem aria-pressed que segue o cursor |
Botão que mostra o estado do último clique mente sobre o texto onde o cursor está — e botão que mente é pior que botão sem estado | aria-pressed alternando no clique (descasa do conteúdo na primeira vez que se move o cursor) |
| ET4 | A área é textbox multilinha, com nome ligado (aria-labelledby) e descrição (aria-describedby) |
contenteditable puro não se anuncia como campo. SC 4.1.2 |
div editável sem papel (a AT não sabe que é campo) |
| ET5 | Os atalhos existem e estão escritos na tela — e também no nome acessível de cada botão | Atalho que ninguém descobre não existe. E atalho escrito e inoperante é pior: promete e falha | Atalhos só na documentação |
| ET6 | Tab SAI do editor e nunca insere tabulação | SC 2.1.2. Indentar lista com Tab é a convenção dos editores — e é exatamente o que prende quem navega por teclado. A indentação, quando for preciso, entra pela toolbar | Tab indenta (armadilha de teclado); Esc para sair (exige saber que existe) |
| ET7 | A saída é HTML semântico de lista branca: p, br, strong, em, ul, ol, li, a |
SC 1.3.1. <b> é aparência, <strong> é relação — e é a relação que sobrevive à reimpressão em três meios |
<b>/<i> (não viajam); HTML livre (superfície de injeção e de drift) |
| ET8 | O que se cola passa pela mesma higienização | Uma função só, usada nos dois caminhos: duas implementações seriam duas verdades, e a que ninguém testa é a que deixa passar | Limpar depois de colar (há uma janela com o conteúdo sujo já no DOM) |
| ET9 | Link sem texto não nasce: a seleção vira o rótulo; e o rel entra sempre |
URL crua lida letra a letra por leitor de tela; “clique aqui” não diz para onde vai | Pedir a URL primeiro e o texto depois (produz link sem rótulo quando a pessoa desiste no meio) |
| ET10 | A dica do vazio não é o rótulo | Regra do §13: placeholder como rótulo some ao digitar e leva o nome do campo junto |
Dica no lugar do rótulo (o padrão que o §13 já proibiu) |
| ET11 | Cor fixa colada não sobrevive — nem como style, nem como atributo |
color:#333333 não inverte no tema escuro: é o BT3 do §64 entrando pelo conteúdo em vez do CSS. E a higienização remove a tag, mantendo o texto — nada do que a pessoa escreveu se perde; perde-se só o que o outro programa impôs |
Manter a cor “porque o usuário escolheu” (ele escolheu no outro programa, para o tema claro dele) |
| ET12 | Alvo dos botões ≥ 44×44px | Piso do projeto. Toolbar é onde a tentação de encolher aparece primeiro | 32px por densidade |
| ET13 | Limite, se houver, é anunciado e não trunca em silêncio | Herda a doutrina do CD4 (§66): truncar sem avisar entrega um texto que a pessoa não escreveu | Corte silencioso no envio |
| ET14 | O valor tem leitura em texto puro derivada em fonte única | A lista, a busca e o e-mail curto consomem texto sem marcas. Sem uma derivação canônica, cada consumidor inventa a sua, e nascem três versões do mesmo texto | Cada consumidor removendo tags do seu jeito |
68.4 Os números medidos#
| Par | Claro | Escuro | Piso |
|---|---|---|---|
| Texto do campo sobre a superfície | 13,72 | 12,49 | 4,50 |
| Botão da toolbar em repouso | 4,60 | 7,39 | 4,50 |
Botão pressionado (sobre action-primary) |
4,60 | 7,39 | 4,50 |
| Barra inferior (contador e dica) | 7,13 | 9,10 | 4,50 |
| Borda do editor (não-textual) | 3,38 | 4,50 | 3,00 |
Alvo dos seis botões: 44×44px.
A prova da higienização, medida — colado o HTML que um editor de escritório produz
(style="font-family:Calibri;color:#333333", <b>, <font color="#FF0000">, <div>, <span>):
| Resultado | |
|---|---|
| Tags que sobraram | p · strong · em |
style= no resultado |
nenhum |
color no resultado |
nenhum |
| Texto perdido | nenhum — “Escopo”, “três quadros de distribuição”, “5 dias úteis” e “Relatório em PDF ao fim” continuam lá |
68.4-b O gate achou o que a lista branca não cobria: comentário também é conteúdo#
O Rafael colou de um editor real e o resultado trouxe <!--StartFragment--> para dentro do
campo. A higienização varria no.querySelectorAll('*') — e comentário não é elemento. Ele
sobrevivia inteiro, e a suíte não via, porque ela media tags e comentário não é tag.
Não é enfeite: <!--StartFragment--> e <!--EndFragment--> são marcadores que o clipboard do
Windows adiciona a todo text/html; e o Word emite comentários condicionais
(<!--[if gte mso 9]><xml>…<![endif]-->) com folhas de estilo inteiras dentro deles. Uma lista
branca de tags não vê nada disso.
Correção: a higienização passa a varrer os nós de comentário com TreeWalker antes de olhar os
elementos, e removê-los. Guarda nova ET8-b, que cola um text/html com os dois marcadores e um
bloco condicional do Word e cobra zero nós de comentário — conferindo, na mesma medida, que
nenhum texto se perdeu. Provada capaz de reprovar: desfeito o conserto, o placar vai a
20 · 1.
Regra nova: guarda que varre por TIPO DE NÓ erra em todos os tipos que não listou. É a mesma família dos defeitos de recorte (8º, 14º, 17º) — o escopo errado, de novo, agora escolhido por tipo em vez de por posição ou por texto.
E um achado de leitura, não de código: o bloco "o que se cola" mostra o código-fonte
escapado. Selecioná-lo com o mouse e dar Ctrl+C leva texto, e o editor insere texto —
corretamente, mas o resultado parece quebrado. A bancada agora diz isso em uma linha, e o botão
"Colar isto no editor" é o caminho declarado: ele entrega o mesmo conteúdo como text/html, que
é o que o editor de escritório de fato coloca na área de transferência.
68.4-c A lista branca responde QUAIS tags — ela não responde EM QUE ORDEM#
Segunda volta do mesmo gate, e o achado é de outra natureza. Colando dentro de um parágrafo existente, a saída era:
<p>Texto que ja<p>Escopo…</p><p>Prazo…</p> estava no campo.</p>
Bloco dentro de bloco. HTML inválido — um <p> não pode conter um <p>. E a guarda ET7
passava, porque o conjunto de tags estava certo: p, strong, em. O que estava errado era o
arranjo.
Por que aconteceu: a colagem insere os nós com range.insertNode, e a API do DOM não impõe
modelo de conteúdo — só o parser de HTML impõe. Inserir programaticamente permite o que o
parser jamais produziria.
Correção, usando o próprio parser: reatribuir innerHTML faz o conteúdo ser reanalisado, e o
parser fecha o <p> de fora ao encontrar o de dentro. Depois disso, parágrafo vazio sai e texto
solto no topo é embrulhado — sem bloco, o consumidor (proposta, e-mail, PDF) não sabe onde o
parágrafo começa. A normalização roda só na colagem, nunca no input: o round-trip recria os
nós, e recriar nós enquanto alguém digita mataria o cursor a cada tecla.
Regra nova: lista branca é vocabulário; estrutura é outra medição. Toda guarda que valida marcação por conjunto de tags precisa de uma irmã que valide aninhamento.
Guarda nova ET7-b: cola com o cursor no meio de um parágrafo — a posição que produzia o
defeito — e mede três coisas que só existem na estrutura (bloco aninhado, parágrafo vazio, nó de
texto solto no topo), conferindo também que o texto antes e o texto depois do cursor sobrevivem.
Provada capaz de reprovar: sem a normalização, ela acusa 2 aninhamentos e o placar vai a
21 · 1.
68.5 Uma decisão de render, e por que ela está escrita#
Os quatro primeiros botões usam glifo tipográfico — B, I, •—, 1—. Os dois últimos usam
palavra: “Link” e “Limpar”.
O motivo é um achado do render, não gosto: ⛓ (link) e ⌫ (limpar) caem em fonte de fallback
e saem como retângulo vazio em parte das máquinas. O set canônico do §45 não tem glifo de link
nem de limpar formatação, e a árvore NM4 manda usar Lucide nesse caso — mas importar dois SVGs
para dois botões de uma toolbar de seis é peso sem retorno. Palavra curta é legível em qualquer
fonte, em qualquer tema, e não precisa de legenda.
68.6 A suíte#
suite-texto-rico.mjs — 22 PASS · 0 FAIL · 1 medida. A guarda central mede o que sai, não o
que aparece: um editor pode parecer perfeito e produzir <b style="color:#333">.
Detalhe de método que vale para as próximas suítes: a lista branca está escrita nas duas pontas — no gerador e na suíte — de propósito. Guarda que importa a régua do medido não mede nada: mede a opinião dele sobre si mesmo. Se alguém afrouxar a lista no gerador, a suíte reprova.
| Guarda | O que ela faz de fato |
|---|---|
ET7 |
seleciona um trecho, clica em negrito e compara as tags da saída com a lista branca |
ET3 |
move o cursor para dentro e para fora do negrito e confere o aria-pressed nos dois |
ET8 |
dispara um paste real com text/html sujo e confere o que sobrou |
ET11 |
confere que nenhuma cor sobreviveu e que nenhum texto se perdeu |
ET6 |
aperta Tab e confere que o foco saiu e que nada foi inserido |
ET5-OP |
aperta Ctrl+B e confere que produziu <strong> |
ET2-OP |
aperta → e confere que o foco andou e que o tab stop acompanhou |
ET9 |
clica em link sem seleção (não nasce) e com seleção (o texto vira rótulo, com rel) |
68.7 Pendências#
| # | Pendência | Por que fica aberta |
|---|---|---|
| ET-P1 | A bancada usa document.execCommand, que é API depreciada |
O que se avalia nesta página são os contratos — o que a toolbar promete, o que o estado reflete, o que a higienização deixa passar —, não a engenharia do editor. Em produção o §68 é implementado sobre biblioteca de edição de verdade. A fronteira está dita no código e aqui, para não virar surpresa |
| ET-P2 | ✅ FECHADA — gate visual APROVADO em 2026-08-17. O olho achou o <!--StartFragment--> atravessando a higienização, e a reprodução do caso revelou <p> dentro de <p>; guardas ET8-b e ET7-b. A seção é promovida a estável |
|
| ET-P3 | O editor de DOCUMENTO (A13) segue AUSENTE | Fronteira declarada em 68.0. Ele precisa do registro tipográfico de leitura (C25: 660px, 16px/1,5, outra stack) e de estrutura de blocos — é outro componente, e provavelmente outro arquétipo de tela |
| ET-P4 | A higienização não foi auditada como fronteira de segurança | Ela existe para proteger a integridade visual e semântica do valor. Sanitização contra injeção é responsabilidade do produto, no servidor, e isso precisa estar escrito onde o produto lê — não só aqui |
| ET-P5 | Os três consumidores (proposta, e-mail, PDF) ainda não declararam que desenham as seis marcas | O ET1 assume que eles sabem. Assumir não é medir — a auditoria dos três é o que fecha o argumento de custo do 68.2 |