# F4.I1.T6 — Editor: <dialog> nativo no lugar de window.prompt

| Campo | Valor |
|---|---|
| **Fase** | F4 — Produto & CMS |
| **Issue** | F4.I1 — CMS próprio e editor (plano 12) |
| **kd task** | `task_01qjjs8s` (`closed`) |
| **Âncora** | `wiki/implementation/04-produto-cms.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- `createTextDialog` em `dialogs.js`; `<dialog>` em `editor.blade.php` (`data-rich-editor-text-dialog`); `dialog_apply` nos 4 locales.
- Teste `RichTextEditorTest::the_editor_never_uses_the_blocking_window_prompt`; E2E cobre upload + legenda.

## Arquivos para revisar

- `src/services/RichText/Assets/js/dialogs.js`
- `src/services/RichText/View/components/editor.blade.php`
- `src/services/RichText/Lang/{pt_BR,en,es,fr}/editor.php`
- `src/services/RichText/Test/RichTextEditorTest.php`
- `src/tests-e2e/tests/08-cms-noticias.spec.ts`

## Pontos de revisão

### PHP 8.4
- [ ] `declare(strict_types=1)`; tipos nativos (union/enum/`readonly`); sem `mixed` desnecessário.
- [ ] Dependências **injetadas** (nenhum `new`/`app()`/`resolve()` no corpo); sem traits/abstract/generics.
- [ ] `match` em vez de cadeias de `if`; `?->`/`??`; sem código morto; PHPStan `max` limpo.

### Laravel 12
- [ ] Nativo primeiro (Eloquent casts/scopes, `FormRequest`, `Gate`, scheduler, events) — sem reimplementar.
- [ ] Sem closure em rota; resources/rotas nomeadas; `config()` só lê, `env()` só em `config/`.
- [ ] Migração **idempotente**; `down()` → `backup_*` (nunca `DROP`); sem `orderBy` cru quando `latest/oldest` serve.

### HTML semântico + a11y (eMAG/WCAG 2.2 AA)
- [ ] Um `<main>`, headings em nível correto, landmarks; só ARIA quando necessário.
- [ ] Alvos ≥ 24 px (≥ 44 px em ponteiro grosso); foco visível; rótulos em todos os campos; contraste (axe).
- [ ] Funciona **sem JS**; `sniffCSS` sem `fail`/`warn` fora do ledger.

### CSS moderno
- [ ] `oklch`, `clamp`, `rem`/`fr`/`dvh`/`%`; `@layer` (a11y por último); `@property`; subgrid com fallback.
- [ ] Sem `!important`, sem `style="…"` inline, sem hex/rgb (exceto justificado); sem CDN.

### Decisões do projeto (ADRs)
- [ ] Respeita a ADR do escopo (ver cabeçalho) e não escapa das demais (001, 002, 004, 013, 015…).
- [ ] Determinismo: relógio/id injetados, ordem explícita, dados idempotentes (ADR-001).

### i18n (ADR-012)
- [ ] Nenhuma string visível fixa; 4 locales com paridade de chaves; `route()`/`localized_url()` (nunca path literal).

### Observabilidade/segurança/testes
- [ ] Span + `log_event('{module}.{action}')` com atributos `scalar|null`, **sem PII** (ADR-024).
- [ ] Teste que falha sem a mudança (vermelho→verde) + evidência no `kd`; `make quality` 2×.

### Específicos desta task

- [ ] Sem `window.prompt/confirm/alert`; foco retorna ao gatilho; `aria` do diálogo.

## Comandos úteis

```bash
make quality            # lint + PHPStan max + gates + testes
make e2e                # Playwright + axe (se a task toca UI)
make check-boundaries   # fronteiras core/services/layout/modules
```

---

> Revisar contra `AGENTS.md`, `wiki/adr/` e o plano citado. Registrar achados no `kd` (`kd write --anchor`) e, se houver correção, teste de regressão.

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-18 | agente | 🔧 | **A** (alto) o título do diálogo de legenda/alt mostrava a **chave crua** (`media_caption`): `toolbar.js` lê `t('media_caption')`/`t('media_alt')` e o componente **não** publicava `data-i18n-media-caption`/`-alt` — o fallback `?? key` do `t()` é silencioso → 2 atributos + **e2e** no título dos dois diálogos + gate estrutural (`every_translation_the_editor_reads_is_published_by_the_component`), ambos **provados vermelhos** — `fact_01pu2gao`; **B** (alto) o `<textarea>` de **cada locale** (o campo principal do Notícias) estava **sem nome acessível** — o rótulo visível aponta para o **radio**, e com JS o editor esconde o textarea (o axe o ignora) enquanto o caminho de degradação não tinha gate nenhum → `aria-label` por locale + **e2e novo** que bloqueia o bundle do editor e roda axe no fallback (vermelho: `[critical] label: Form elements must have labels`) + asserção estrutural — `fact_0150v6t6`; **C** (médio) o comentário/docblock prometia “foco devolvido ao gatilho”, mas **medido no browser**: Cancelar e `Esc` devolvem ao gatilho, **aplicar deixa o foco no editor** (o `focus()` do TipTap é agendado em rAF e vence o fechamento) → comentários corrigidos para o contrato real + e2e fixa os **três** caminhos — `fact_014holhc`; **D** (médio) `openDialog` registra os listeners de `[data-dialog-close]` **a cada abertura** com `{ once: true }`; aplicar/`Esc` nunca os consome → acumulam com `opener` antigo → `task_00w29li2`, `risk_01cavrsa`; **E** (baixo) `$required` é silenciosamente ignorado no modo multi-locale (o `*` fica `aria-hidden` e nada é anunciado; nenhum chamador combina os dois e `content` é `nullable` no servidor); **F** (obs) os inputs dos diálogos vivem no formulário da página (`file`/`alt`/`caption`/`url`… são enviados no POST; o `file` é limpo após inserir, mas um arquivo escolhido e abandonado sobe junto); **G** (obs) `maxlength="255"` é só do cliente. Verificados e OK: **zero** `window.prompt/confirm/alert` (o teste cobria só `prompt` — passou a cobrir as três, provado vermelho por injeção; mais grep nos 5 JS), `<dialog>` nativo com `showModal()`/`Esc`, sem `<form>` aninhado (assercão), sem `<script>` inline/`on*`/`style=` (assercão), 4 locales com paridade total (76 chaves) e `dialog_apply` presente, preview em iframe `sandbox` sem scripts e excluído do axe, CSRF no fetch de upload e mesma origem. |

> Resultado: `✅ revisado` · `🔧 com correções` · `⏭️ delegado` (com justificativa).
