# F4.I1.T2 — Editor rico TipTap (AST → jsonb, parser semântico, preview) — ADR-028

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

## O que foi implementado

- TipTap 3 vendorizado; AST JSON → `jsonb`; `ContentASTParser` (um método por nó/marca), classes em vez de estilo inline, marcas em ordem canônica, links com esquema restrito; `toText()`.
- Mídia na mesma origem (`media.stream`), upload (`StoreEditorMedia`, AVIF/WebP via libvips), diálogos `<dialog>` nativos, preview split-screen pelo mesmo parser.
- `<textarea>` preservado como fallback **sem JS**; `LocaleCast` + abas por locale.

## Arquivos para revisar

- `src/services/RichText/ContentASTParser.php`
- `src/services/RichText/Contracts/{RichTextRenderer,MediaUrlResolver}.php`
- `src/services/RichText/Media/MediaStreamUrls.php`
- `src/services/RichText/Assets/js/{editor,schema,toolbar,dialogs,preview}.js`
- `src/services/RichText/Assets/css/{editor,content}.css`
- `src/services/RichText/View/components/editor.blade.php`
- `src/services/RichText/Lang/{pt_BR,en,es,fr}/{editor,parser}.php`
- `src/services/RichText/Test/*`
- `src/services/RichText/README.md`
- `src/services/Media/Actions/StoreEditorMedia.php`
- `src/services/Media/Contracts/EditorMediaStore.php`
- `src/services/Media/Http/{MediaUploadController,MediaStreamController}.php`
- `src/services/Media/Http/Requests/MediaUploadRequest.php`
- `src/core/Localization/LocaleCast.php`
- `config/richtext.php`
- `tests/Feature/LocaleCastTest.php`

## 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 nó de HTML livre**; HTML determinístico (ordem canônica de marcas).
- [ ] Editor sem APIs bloqueantes (`window.prompt` → `<dialog>`); sem `<form>` aninhado; `injectCSS: false`.
- [ ] MIME decidido no servidor (finfo); vídeo fora (YouTube é link).

## 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 botão `underline` chamava `toggleUnderline`, mas o `StarterKit` estava configurado com `underline: false` e o `@tiptap/extension-underline` **nunca era importado** (declarado e não usado) → o clique lançava `TypeError` → corrigido + **gate novo** `RichTextSchemaTest` (comando da toolbar × provedor no schema; pacote TipTap declarado precisa ser usado) — `fact_01bg998i`; **B** (alto) a **mídia não persistia**: o bucket não existia, `store()` caía no disco **local** (19 fallbacks silenciosos, contra ADR-030) e `retrieve()` **não tinha fallback** → **toda** mídia enviada dava 404 e nenhum gate acusava → corrigido (leitura espelha a escrita), `make up` passa a garantir o bucket (paridade com `up_prod.sh`) e o e2e passou a exigir `naturalWidth > 0` (provado vermelho) — `fact_00cw7pqg`; **C** (alto) o editor montava a mídia com a origem do **portal** e a CSP do painel (`img-src 'self'`) **bloqueava a imagem** — o autor nunca via o que inseriu → corrigido (rota `media.stream.cms` + URLs **relativas** no resolver e no template) — `fact_002cge80`; **D** (médio) SVG servido `inline` como `image/svg+xml` confiando **só** na CSP do edge → a resposta agora declara `default-src 'none'; sandbox` (`fact_01txq6ym`); **E** (baixo) `isAllowedUri` do cliente mais frouxo que o `isSafeUrl` do servidor → alinhado (`fact_00hse5nh`); **F** (baixo) `__index` morto em `htmlMarks` → removido; **G** (obs) `data-indent` sem CSS no editor (`risk_00fao013`); **H** (obs) `BASE_URL` default do e2e é 8080 (outro serviço) (`risk_00v9atym`). Verificados e OK: `injectCSS: false`, zero `window.prompt/confirm/alert`, um único `<form>`, sem `innerHTML`/`eval`, marcas em ordem canônica estável, links com esquema restrito e `rel` explícito, YouTube como **link** (sem iframe), MIME decidido no servidor (finfo), `<textarea>` como campo sem JS, 4 locales do editor, config × constantes. |

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