# ADR-016 — Tema e layout são dados, não código

| Campo | Valor |
|---|---|
| **Status** | Aceito (F3) |
| **Data** | 2026-10-14 |
| **Plano de origem** | `wiki/plan/01-analise-e-decisoes.md` |
| **Relacionadas** | ADR-009, ADR-010, ADR-012, ADR-017 |

## Contexto

O legado (`prefeituras_v5`) guardava **HTML de preview e CSS** na tabela `layouts`
(`layouts.value`, `layouts.style`) e usava **flags de UI como colunas** (ex.: `is_highlighted`).
Isso prendia a apresentação aos dados e exigia deploy para mudar layout. O `sucesu-cms` já
persistia tema/layout em banco, mas ainda com HTML. É preciso trocar tema/layout **sem deploy**
e manter o conteúdo agnóstico.

## Decisão

- Presets em Enum (`Layout\Engine\LayoutPreset`); **slots** registrados em runtime; configuração
  (ordem/visibilidade/conteúdo, tokens de tema) em **JSONB**, editável no CMS. Trocar tema/layout =
  dado + CSS vars, sem deploy.
- **Escopo com precedência determinística:** `default do tipo → global → preset → perfil de URL →
  instância`, e a **acessibilidade sempre por último** (vence toda a cadeia).
- **Slot = pilha ordenada:** empilha widgets em ordem com `gap` do design system; dois widgets à
  direita = posições 1 e 2. **Sem posição absoluta/pixel.**
- **Três camadas distintas — estrutura ≠ conteúdo ≠ dados:**
  - **Estrutura** (`src/layout`): shell (`skip links`, barra de acessibilidade, `header/nav/main/
    aside/footer`), regiões do preset, grid, temas, a11y e os **espaços (slots)**. A estrutura
    **governa** header, rodapé, áreas laterais, acessibilidade e SEO de base — o módulo **nunca**
    toca neles.
  - **Conteúdo** (módulo): implementa **somente o `<main>`** — o fragmento central — e **declara os
    campos padrão esperados** pelo componente do shell (título, descrição, breadcrumbs, canonical,
    robots, imagem, `schemas`). Não monta shell, sidebars nem barra de acessibilidade.
  - **Dados** (entidades/JSONB): nada de HTML/CSS/classes (ADR-017).
- **Contrato único e óbvio (portal e CMS):** um componente de shell por área —
  `<x-layout::portal>` (público) e `<x-layout::cms>` (admin) — com **`@props` tipados, defaults
  determinísticos e um único slot de conteúdo**. O módulo fornece metadados + conteúdo; a estrutura
  resolve preset, grid, sidebars (slots), a11y e SEO. A tradução segue o **dono do fragmento**
  (ADR-012): shell = namespace do layout; fragmento = namespace do módulo; compartilhado = namespace
  próprio, **nunca duplicado**.
- **Acessibilidade = camada universal, agnóstica ao módulo** (`@layer a11y` por último, ADR-010):
  alto contraste (P&B), dislexia, espaçamento ampliável, imagens em P&B e movimento reduzido são
  **preferências do usuário** que **sobrepõem cores, tipografia, contraste e imagens** em qualquer
  tela — **sem alterar a estrutura**. Baseline sem JS via `prefers-contrast`/`forced-colors`/
  `prefers-reduced-motion`; preferências opt-in persistidas e aplicadas **antes do primeiro paint**.
- **Overrides limitados por schema:** o CMS só aceita **tokens semânticos validados** (`cor`,
  `borda`, `raio`, `densidade`, `espaçamento`, `tipografia`), restritos a **escalas/enums** e faixas
  seguras — nunca valor livre que quebre o layout (nem CSS/HTML).
- **Overrides locais = JSON de tokens** (`cor`, `borda`, `espacamento`, `variante`…) — nunca CSS/HTML.
- **Perfil por grupo de URL:** `LayoutProfile` (meta-serviço `services/Layout`) resolve
  `rota → perfil`; o módulo **só declara o grupo** (ex.: `/noticias`), sem layout próprio.
- **Dois escopos de tema:** `cms` e `portal`.
- **`PresetSchema`** cobre paleta, tipografia, raio, sombra, espaçamento, densidade, movimento e
  variantes; presets iniciais extraídos dos modelos 1..6.
- **Implementação atual (T5):** o schema do tenant em `config/design.php` aplica **cor, raio e
  `font_content`**; `borda`/`densidade`/`espaçamento` ficam no `PresetSchema`/presets (backlog).
  `--font-content` é token de tipografia de conteúdo, não `--tenant-*`.
- **Widget/Banner = tipo registrado**; conteúdo é **estrutura atômica (JSON)**. A fonte implementa
  `Core\Contracts\Ports\WidgetSource` (`id` namespaceado, `title`, `icon`, `slots`, `embeddable`,
  `render`) e registra-se no `WidgetRegistry` do Core; o layout renderiza por `id`
  (`<x-layout::widget id="…" />`).
- **Widget de outro módulo = provider + placement** (registry do Core; sem import entre módulos).
  Widget de módulo **inativo** não é oferecido nem renderizado (fail-open — ADR-034).
- **Home é módulo:** a página inicial é um módulo próprio que usa a mesma filosofia — só controla a
  área central e como usa a estrutura.

## Justificativa (alternativas descartadas)

- **Layout/CSS em código por modelo** — descartada: exige deploy e duplica a cada cliente.
- **HTML/CSS no banco** — descartada: acopla dados à UI (erro do legado).
- **Canvas livre com posição absoluta** — descartada: quebra responsividade e acessibilidade.

## Consequências

**Positivas**
- Mudança de tema/layout sem deploy; conteúdo agnóstico; um perfil serve todo o grupo de URL.

**Custos / negativas**
- Exige um engine de slots + resolução de perfil (uma vez).
- Configuração demais pode confundir → modo simples × avançado.

## Exemplo mínimo (comparativo)

```jsonc
// ❌ Legado: HTML/CSS no banco
{ "key": "sidebar", "value": "<div style='width:100%'>…</div>", "style": ".x{color:red}" }

// ✅ Dado: pilha ordenada + tokens semânticos
{ "slot": "main.left", "widget": "noticia.mais-acessadas", "ordem": 1,
  "tokens": { "cor": "secondary", "borda": "suave", "espacamento": "compacto" } }
```

## Verificação

- Trocar preset muda o portal sem deploy; a11y (dislexia/contraste) continua vencendo.
- **Teste estrutural (gate):** toda view de módulo usa **um** componente de shell (`<x-layout::portal>`
  ou `<x-layout::cms>`) e **não** contém `<html>`, `<head>`, `<aside>`, `header`/`footer` de shell,
  barra de acessibilidade ou `<style>`/`style="…"`.
- **Teste estrutural:** nenhum módulo importa outro módulo; widgets/banners só via Core.
- **Teste:** o shell renderiza com/sem JS; a11y (P&B, dislexia, espaçamento, grayscale) altera só
  tokens/`filter` e **não** a marcação.
- `tools/migration-check`/teste garante ausência de HTML/CSS no banco.

## Emenda 2026-10-20 — camadas configuradas e cache do `theme.css`

- **As 5 camadas têm fonte explícita enquanto `services/Theme`/`LayoutProfile` são backlog:**
  `ThemeResolver::configuredLayers()` lê `design.overrides`, `design.preset_overrides`,
  `design.url_profile_overrides` e `design.instance_overrides`. `GET /theme.css` resolve a cadeia
  completa; o shell só referencia a folha quando **algum** valor passa pelo `ThemeSchema`
  (`hasConfiguredOverrides()`), não pelo simples `filled()`.
- **Defaults também passam pelo schema:** default ausente/inválido não vira `--x: ;`; o DTO filtra
  valores vazios e o CSS não emite propriedade sem valor.
- **`/theme.css` revalida:** `Cache-Control: public, no-cache, must-revalidate` + `ETag`; o
  `If-None-Match` usa o parser nativo (`Response::isNotModified`), aceitando `W/` e listas. Mudança
  no painel reflete na próxima navegação, sem esperar 1 h de frescura.
- **Orçamento de stylesheet por cenário:** 1 folha da Vite + no máximo 1 `theme.css`; o e2e conta
  as duas separadamente.

## Emenda 2026-10-23 — edição profunda só em Home/Páginas; layout por área

- **Edição de blocos é exceção:** somente `Portal/Home` e `Portal/Paginas` usam o catálogo fechado
  de blocos (`{version, sections:[{id,type,props,hidden}]}`, schema por tipo). Os demais módulos
  **não** recebem editor de seções/blocos; eles declaram widgets e o posicionamento é feito pela
  ação **“Configurar layout da área/módulo”**.
- **Layout por área:** a ação abre a mesma superfície de layout (`LayoutProfile`/slots), escopada à
  rota/módulo, e edita **posições de widgets** — nunca conteúdo do módulo. Precedência:
  perfil da área/módulo → configuração global definida pelo Suporte **na área de Suporte** →
  default do preset. Sem
  posição absoluta; a ordem é a pilha do slot e o DOM acompanha a posição visual.
- **Sem permissão nova por módulo:** a autorização é da superfície de layout (Suporte/admin); o
  módulo só declara o escopo e os widgets que oferece. Widget de módulo inativo não aparece.
- **Tema de widgets é dado:** `theme.widgets` (mapa por id/tipo) usa tokens validados pelo
  `ThemeSchema`/schema do widget; nada de CSS/HTML/classes no banco (ADR-017/046).
- **Home/Páginas continuam com preview autenticado e publicação versionada;** tipo desconhecido,
  `props` inválida ou `hidden` seguem a regra da ADR-016 (ignorar/preservar posição).

## Emenda 2026-10-25 — documento de página e catálogo fechado de 12 blocos (P5)

- **Documento é dado:** página/Home é `{version, sections:[{id, type, props, hidden}]}`. A ordem do
  array é a ordem visual; **`hidden` preserva a posição** (a seção oculta continua na lista) e
  **não existe posição absoluta/coordenada** — nada de `position`/`top`/`left`/`inset`/`z_index`.
- **`props` validado por schema fechado por tipo:** campo desconhecido é descartado; campo inválido
  (tipo, limite, enum, URL insegura, id Media inválido) invalida **o bloco inteiro**, que é
  ignorado — nunca se renderiza meio bloco.
- **Proibições que invalidam o bloco:** `html`, `template`, `iframe`, `css`/`style`/`class`,
  `script`, nó de HTML livre no AST, marcação HTML/Blade em texto puro e chaves de posição.
  Blade/CSS/HTML continuam **fora do banco** (ADR-017/028); texto rico é só AST.
- **Tipo desconhecido é ignorado** e **`version` desconhecida não produz documento**: o chamador cai
  no default (parser `ParseHomepage`, S1.2) — dado corrompido nunca deixa a página vazia nem quebra
  o request.
- **Catálogo fechado inicial (12 tipos):** `hero`, `quick-links`, `text`, `text-image`,
  `cards-links`, `cards-numbered`, `steps`, `faq`, `video`, `latest-news`, `highlights`, `banner`.
  Cada tipo declara schema de `props`, **componente Blade concreto**, campos de mídia (**id Media**,
  ADR-026) e regras de acessibilidade. Acrescentar um tipo exige ADR — não é configuração de tenant.
- **Camada:** catálogo e componentes vivem em `src/layout/Blocks` e `src/layout/Components/blocks`
  (apresentação); `Portal/Home` e `Portal/Paginas` consomem `Layout\Blocks\*` e **não importam um ao
  outro** (ADR-013). Nenhum contrato novo no Core.
- **Acessibilidade (ADR-025):** um único `<h1>` por página (só o `hero`), headings em ordem, listas
  semânticas, `alt` obrigatório, propósito do link explícito, **sem autoplay e sem carrossel
  automático**; todos os blocos degradam **sem JS** (`<details>` nativo no FAQ; vídeo é link — D12 —
  nunca `iframe`).

## Emenda 2026-10-25 (S2.2) — conteúdo traduzível, publicação e prévia autenticada

- **Conteúdo por locale é dado, sem mudar o catálogo.** Campos marcados `translatable` aceitam o
  mapa `{"pt_BR": …, "en": …, "es": …, "fr": …}`; chave fora dos locales suportados é descartada e
  cada valor é validado pelo **mesmo tipo** do campo (texto/AST). A forma simples (valor único) é a
  forma histórica e vale como conteúdo do locale default — documentos da S2.1 continuam válidos.
  Marcação HTML/Blade/CSS continua proibida (também dentro de qualquer locale) e o schema fechado
  segue sendo a única porta de validação.
- **Resolução determinística no renderizador:** locale vigente → locale default →
  primeiro locale preenchido na ordem canônica (`BlockLocalizer`). O locale vem da URL (ADR-012);
  o CMS edita os 4 locales por campo e a prévia autenticada aceita `?locale=`.
- **Publicação versionada:** o rascunho (`draft`) é copiado para `published` em
  `home.home_documents` como **documento canônico validado**; publicar exige `home.aprovar`, grava
  a evidência `home.published` (ADR-035) e recusa documento sem seção visível (o default é o
  fallback de “não publicado”, não de “publicado vazio”). A home pública lê **somente** `published`.
- **Cache/invalidação:** a home pública usa chave versionada (`home.published.{lock_version}`) e a
  publicação esquece a versão substituída de forma síncrona; preview não entra em cache público.
- **Prévia autenticada:** rota na área CMS (sessão + `home.ver`), sem token público, renderizada no
  shell real do portal com o mesmo `<x-layout::blocks>` e `noindex, nofollow`; cache aplicacional
  curto escopado por usuário/permissões/versão do rascunho (nunca HTTP público).

## Referências

- `prefeituras_v5` (`layouts`, `LayoutController`); `wiki/plan/10-layout-engine-slots.md`, `wiki/plan/16-front-builder-widgets-banners.md`.
