---
fonte: 01-canonicos/seed-componentes.md
versao_da_fonte: v1.43
secao: 68
titulo: "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**"
sequencia: 74 de 98
bytes_do_corpo: 16533
md5_do_corpo: e4f9203aaaf29dbe218f1743eadd2a47
gerado_por: 06-validacao/geradores/gen-camada-ia.py
nota: fatia GERADA — o corpo abaixo é byte a byte o trecho do canônico; edite o canônico, nunca esta fatia. Canônico inteiro em https://ds.seed.eng.br/01-canonicos/seed-componentes.md
---
## 68. 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.md` registra, 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 |

---

