# ADR-036 — Shell do CMS como dado: identidade, regiões e posição da navegação

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-17) |
| **Data** | 2026-10-17 |
| **Plano de origem** | `wiki/plan/12-admin-cms-editor.md` · `wiki/implementation/04-produto-cms.md` |
| **Relacionadas** | ADR-001, ADR-002, ADR-009, ADR-010, ADR-016, ADR-022, ADR-025, ADR-026 |

## Contexto

A ADR-009 fixou o CMS como **shell próprio e independente** (`src/layout/Cms`, namespace `Layout\Cms`,
views `cms::`) com a filosofia **“espaços, não controle”**: o módulo usa `<x-layout::cms>`, informa
`title`/`breadcrumbs` e preenche **apenas** o `<main>`. A ADR-016 fez o mesmo para o **portal**, mas lá
o layout é **dado** (default → global → preset → URL → instância + slots + tema em `jsonb`).

O CMS, porém, ficou **fixo em código**: header, rodapé e sidebar **sempre presentes**, sidebar
**sempre à esquerda** (drawer), identidade = `config('app.name')`, títulos/menus com texto fixo em
português. Uma instância = um cliente: personalizar o painel hoje exigiria **fork de shell** — e o
projeto proíbe duplicar estrutura (ADR-001/ADR-009).

Além disso, o painel é usado em telas pequenas (tablet em secretaria), onde uma coluna fixa de
navegação custa espaço útil. O pedido de produto é: **cor, logo e título configuráveis**; **presença e
posição** de header/rodapé/sidebar configuráveis; sidebar podendo ir para **esquerda, direita ou um
menu acionável em órbita** — sempre com **estabilidade** (sem salto de layout, sem depender de JS).

## Decisão

**1. Identidade do CMS é dado, resolvida para tokens.** `config/cms.php` (e, na instância, o mesmo
array vindo do banco) define `title`, `logo`, `color_primary`, `color_accent`. `null` = **herda** o
tema do tenant (`--tenant-*`, ADR-016). O resolver valida a **forma** (cor `oklch()`, sem `;`/`{}`/
`url()`/`@`); valor inválido é **ignorado** (a camada anterior vence) — configuração nunca quebra o
shell, mesma semântica do `ThemeResolver`. A identidade é publicada como **CSS custom property**
(`--cms-*`) em folha **same-origin** (`GET /cms-theme.css`, `ETag`/`304`), nunca em `<style>` inline
(CSP limpa — ADR-022); sem override, **nenhuma requisição extra** é feita.

**2. Regiões são dado.** `header.enabled`, `footer.enabled`, `sidebar.enabled` +
`sidebar.position ∈ {left, right, orbital}`. A estrutura é **função pura** dessa configuração: as
parciais são incluídas ou não, e a posição decide **onde** — e nada mais. Precedência mínima:
`default (config) → instância`; **preferência de usuário nunca muda estrutura** (no máximo estado:
colapsado/aberto, tema claro/escuro), para que a tela não mude de forma entre usuários.

**3. Ordem do DOM acompanha a posição (WCAG 2.4.3).** Sidebar `left` → no DOM **antes** do `<main>`;
`right` → **depois**; assim a **ordem de foco** coincide com a ordem visual. O **módulo** permanece
intocado (só preenche `<main>`) — o contrato “espaços, não controle” continua válido.

**4. Posição é CSS, não medição.** Largura/coluna via custom property (`--cms-sidebar-width`),
grade CSS com propriedades **lógicas** (`inset-inline-*`, `grid-template-columns`), sem JS de
medição e sem reflow: **CLS 0**. Em telas pequenas, `left`/`right` viram painel off-canvas
ativado por botão (checkbox + `label`, **sem JS**); `orbital` usa um **botão flutuante** fora do
fluxo (não desloca conteúdo).

**5. Órbita é progressive enhancement (nunca o único caminho).** O modo `orbital` renderiza a
**mesma** `<nav>` com lista de links — operável **sem JS** — e, com suporte a **Popover API**
(`popover`/`popovertarget`), a mesma marcação passa a se apresentar como **menu acionável** ancorado ao
botão: `aria-expanded` do invocador é gerenciado nativamente, `Esc` fecha, foco retorna ao botão,
`prefers-reduced-motion` desliga a transição, e o *skip link* + landmarks continuam valendo. Sem
suporte a Popover, o conteúdo permanece **visível e utilizável** (fallback é o menu comum). Zero
biblioteca, zero JS de estado.

**6. i18n do shell é obrigatória.** Todo texto do shell vem de `cms::shell.*`
(`src/layout/Cms/Lang/{pt_BR,en,es,fr}/shell.php`); o gate de texto fixo (F3.I4) passa a cobrir
`src/layout/Cms/**`.

**7. Determinismo e verificação.** O shell é função pura de (configuração + manifesto) → `CmsShellTest`
estrutural (presença de regiões, ordem do DOM por posição, fallback da órbita, identidade publicada) e
a11y no gate (ADR-025).

## Alternativas descartadas

- **Fork de shell por cliente / pacote de tema** — descartada: duplica estrutura e mata a manutenção
  única (ADR-001/ADR-009).
- **Modos “fixed/boxed/top-nav” ao estilo AdminLTE** — descartada: superfície grande, demanda
  inexistente; presença de regiões + 3 posições cobrem o caso real com muito menos código.
- **Painel reativo (Livewire/Alpine) para mover a sidebar** — descartada: ADR-009 segue valendo; o
  que precisa de interatividade (a órbita) é **nativo** (Popover API) e degrada sem JS.
- **Posicionar por JS (medir e aplicar)** — descartada: layout dirigido por script gera CLS, quebra
  sem JS e não é determinístico (ADR-001).
- **Estrutura por preferência de usuário** — descartada: um usuário escondendo o header muda o
  contrato de navegação da instância; preferência fica restrita a estado.
- **Órbita como único menu (mobile-first radical)** — descartada: caminho único de navegação em
  padrão não convencional é risco de acessibilidade (WCAG 2.4.1/2.1.1); mantida como **modo** com
  fallback.

## Consequências

**Positivas**
- Personalização por instância **sem tocar em código** e sem fork; painel igual em dev/prod.
- Navegação utilizável sem JS, com CLS 0 e ordem de foco coerente em qualquer posição.
- Mesma linguagem de decisão do portal (ADR-016): estrutura é dado; tokens, não CSS no banco.

**Custos / negativas**
- Mais um ponto de configuração a documentar e testar (`config/cms.php` + `CmsShellResolver`).
- A tela de configuração do CMS (F5) precisará **converter** cor escolhida no seletor para `oklch()`
  antes de gravar (a identidade não aceita `hex` — é o padrão do projeto).
- O modo `orbital` tem custo de CSS/`@supports` e precisa de teste de fallback (sem Popover).

## Verificação

- `CmsShellTest`: identidade (`title`/`logo`/`--cms-*` e herança quando `null`), presença das regiões,
  ordem do DOM em `left`/`right`, órbita com e sem `popover`, e recusa de valor inválido (ignorado).
- i18n: paridade das 4 locales de `cms::shell` + ausência de texto fixo em `src/layout/Cms`.
- Gate de acessibilidade (axe/eMAG/sniff) sobre uma tela do CMS, sem `serious`/`critical`.
