---
id: MOD-SUPPORT-STRUCTURALLANDINGEDITOR-UI
tipo: ui
onda: 3
status: concluido
unidades: [MOD-SUPPORT-STRUCTURALLANDINGEDITOR]
fontes:
  - V2/Modules/Support/StructuralLandingEditor/Views/
  - V2/Modules/Support/StructuralLandingEditor/Routes/cms.php
  - V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php
depende_de: [UI-CMS, SVC-GALLERY, SVC-MEDIA, SVC-ACTIVITYLOGGER]
consumidores: [MOD-SUPPORT-STRUCTURALLANDINGEDITOR, MOD-SUPPORT-SUPPORTCONFIGS]
criticidade: media
dono: PageSle
---

# UI — Editor Estrutural (SLE)

> Contrato das 5 telas de submódulo, das 5 abas do editor, dos 7 painéis de layout base,
> dos 6 painéis de user configs e dos 4 componentes. Cobre DOM, eventos JS, estados,
> acessibilidade e receitas de extensão. Núcleo lógico em [`spec.md`](spec.md); payloads em
> [`editores-temas-e-assets.md`](editores-temas-e-assets.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Entrega ao suporte telas de formulário que gravam configuração e código do portal, com construtores JS para dados em árvore (links, redes, LAI) e uma biblioteca lateral de assets com cópia de snippet. |
| Entrada | `$contents` (editor), `$configs`/`$userConfigs`/`$layoutConfigs` (abas), `$assets`/`$type` (fragmento AJAX), GET/POST do usuário, eventos de CodeMirror e do Gallery Picker. |
| Saída | HTML no `v2-cms::layouts.app`; fragmentos de cards; JSON de assets; submissões multipart; flash de sessão. |
| Efeitos colaterais | `@push('scripts')`/`@push('styles')` no layout CMS; carregamento de jQuery 3.7.1 `defer` na sidebar; modais Bootstrap; `beforeunload` armado quando há edição. |
| Particularidade central | A tela do editor mantém **um formulário por aba** e um detector global de “alterações não salvas” que intercepta links, fechamento de aba e troca de tab. |
| Como se integra | `@extends('v2-cms::layouts.app')`; namespaces `v2-structurallandingeditor`, `v2-cms`, `v2-service-media`, `v2-service-gallery`, `v2-service-activitylogger`; rotas `v2.structural-editor.*`. |
| Se removido, quebra | O CMS perde todas as telas de configuração do portal; o manual do editor (`help-manual`) e a File Library deixam de existir. |
| Superfície pública | Views `cms.submodules.*`, `cms.tabs.*`, `cms.components.*`, `manual.content`; IDs e eventos listados em §2.4. |

## 1. Efeito

### 1.1 Garantias para todas as telas

- Herdam `v2-cms::layouts.app` com `title`/`subtitle`, breadcrumb (`page-breadcrumbs`) e
  `content`; as telas principais incluem `flash::message`.
- Rodapé de atividades: `@include('v2-cms::partials.activity-footer', ['model' => new StructuralPortalContent()])`
  em todas as telas de submódulo (`submodules/meta/index.blade.php:124-126`,
  `theme/index.blade.php:136-138`, `structural-data/index.blade.php:156-158`,
  `features/index.blade.php:206-208`, `editor/index.blade.php:182-184`).
- Bootstrap 5 tabs (`data-bs-toggle="tab"`), cards `shadow-sm border-0`, botões
  `btn-primary fw-bold`.
- Formulários usam `csrf_field()`; nenhuma tela implementa validação client-side além de
  `pattern`/`maxlength`/`accept`/`required` de HTML.

### 1.2 O que muda por tela

| Tela | Abas | Formulários | Ação de escrita |
|---|---|---|---|
| Meta | Metadados, SEO, E-mail | 3 (um por aba) para `meta.save` | `UserConfigsService` |
| Tema | Tema CMS, Tema Portal, Banner Global | 2 para `theme.save`, 1 para `theme.save` (logos) e 1 oculto de reset | `UserConfigsService` / `LayoutBaseCmsController` / SupportConfigs |
| Dados Estruturais | Links Header, Links Footer, Contato, Redes Sociais, Transparência (LAI) | 5, todos para `structural-data.save` | `LayoutBaseCmsController` |
| Features | — (card único) | 1 para `features.save` | `UserConfigsService` + `lai_enabled` |
| Editor | Header, Barras Laterais, Footer, CSS, JS | 5, todos para `editor.save` | `StructuralPortalContent` |
| Assets | — (fragmento) | — | leitura AJAX |

### 1.3 O que a UI NÃO faz

- Não pré-visualiza o bloco nem o tema: “as alterações refletem imediatamente” é promessa
  textual, não preview.
- Não valida HTML/Blade/CSS/JS (CodeMirror só colore).
- Não impede perda de dados em todos os casos: o detector cobre links e troca de aba, mas
  não `Ctrl+R` após já ter confirmado uma vez (`sleepUnsavedFlag` é resetado no submit).
- Não tem paginação, busca ou filtro na File Library (lista completa, ordenada por
  `created_at desc`).
- Não mostra a senha SMTP; mostra apenas “••••••••” quando existe.

## 2. Abordagem

### 2.1 Hierarquia de composição

```mermaid
flowchart TD
  app[v2-cms::layouts.app] --> meta[submodules/meta/index]
  app --> theme[submodules/theme/index]
  app --> sd[submodules/structural-data/index]
  app --> feat[submodules/features/index]
  app --> ed[submodules/editor/index]
  meta --> uc1[userconfigs/metadados]
  meta --> uc2[userconfigs/seo]
  meta --> uc3[userconfigs/email]
  theme --> uc4[userconfigs/tema-cms]
  theme --> uc5[userconfigs/tema-portal]
  theme --> lb1[layoutbase/logos]
  theme --> lb2[layoutbase/comunicado-global]
  sd --> lb3[layoutbase/links-header]
  sd --> lb4[layoutbase/links-footer]
  sd --> lb5[layoutbase/contato]
  sd --> lb6[layoutbase/redes-sociais]
  sd --> lb7[layoutbase/transparencia-lai]
  lb3 --> s1[scripts/links-header]
  lb4 --> s2[scripts/links-footer]
  lb6 --> s3[scripts/redes-sociais]
  lb7 --> s4[scripts/transparencia-lai]
  ed --> t1[cms/tabs/header]
  ed --> t2[cms/tabs/main]
  ed --> t3[cms/tabs/footer]
  ed --> t4[cms/tabs/css-head]
  ed --> t5[cms/tabs/js-foot]
  ed --> side[cms/components/asset-sidebar]
  ed --> help[cms/components/help-manual]
  help --> manual[manual/content]
  side --> card[cms/components/asset-card]
```

### 2.2 Ordem de carregamento

1. Layout CMS (Bootstrap CSS/JS, dependências do painel).
2. Cabeçalho + abas (HTML estático).
3. Painéis: cada parcial inclui seu `<form>`; as de layout base empilham um
   `<textarea hidden>` com o JSON atual.
4. `@push('scripts')` de cada parcial (ordem de inclusão): scripts das abas de layout base,
   scripts do editor, scripts da sidebar.
5. `asset-sidebar` empurra jQuery 3.7.1 `defer` **antes** do seu próprio script inline, para
   garantir o Gallery Picker (que depende de jQuery) — mas scripts inline não esperam
   `defer`; por isso o botão faz *polling* de `typeof jQuery` a cada 100 ms por até 5 s.
6. CodeMirror é carregado por `v2-cms::partials.code-editor` (não por este módulo); cada
   editor dispara `sle:editor-change` no `document` quando fica pronto.

### 2.3 Estrutura de uma aba de configuração

Padrão `v2-cms::components.box` (todas as de user configs) ou `<form>` direto + campo
hidden `tab` (todas as de layout base). O `tab` é a única informação que o controller usa
para escolher o handler:

```blade
{{-- V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/contato.blade.php:1-6 --}}
@inject('activityLogger', 'V2\Services\ActivityLogger\ActivityLoggerService')
<form action="{{ route('v2.structural-editor.structural-data.save') }}" method="POST">
    {{ csrf_field() }}
    <input type="hidden" name="tab" value="contato">
    @php
        $configModel = $configs->has('contato') ? $configs['contato'] : null;
        $payload = $configModel ? $configModel->payload : [];
    @endphp
```

Cada parcial de layout base termina com:

```blade
@include('v2-structurallandingeditor::cms.tabs.layoutbase.components.history-table', ['configModel' => $configModel, 'logSuffix' => 'Atualizou os dados de Contato.'])
```

### 2.4 DOM, eventos e AJAX

| ID/classe/seletor | Onde | Papel |
|---|---|---|
| `#metaTabs`, `#tab_meta_metadados|seo|email` | `submodules/meta` | navegação |
| `#themeTabs`, `#tab_theme_cms|theme_portal|theme_banner`, `#reset-cms-form` | `submodules/theme` | navegação + reset |
| `#structuralTabs`, `#tab_sd_*` | `submodules/structural-data` | navegação |
| `#sleEditorTabs`, `#tab_sle_header|main|footer|css|js` | `submodules/editor` | navegação |
| `#sleUnsavedModal`, `#sleUnsavedSaveBtn`, `#sleUnsavedDiscardBtn` | `submodules/editor` | confirmação de saída |
| `.sle-area-editor[data-area]` | abas do editor | agrupa editor + switch |
| `#sleSidebarTabs`, `#sbar-css|js|img`, `.sle-sbar-list[data-asset-type]` | `components/asset-sidebar` | abas e containers da File Library |
| `#sleSbarGalleryBtn`, `#picker_sle_sidebar` | sidebar | abrir picker da galeria |
| `.sle-asset-card[data-asset-id][data-asset-type]` | `asset-card` | card de asset |
| `.copy-snippet-btn`, `.sle-snippet-bar[data-snippet]` | `asset-card` | copiar snippet |
| `#header-list`, `#header_links_json`, `#btn-add-menu`, `#form-header` | links-header | árvore de menu |
| `#footer-list`, `#footer_links_json`, `#btn-add-column`, `#form-footer` | links-footer | colunas |
| `#social-list`, `#social_media_json`, `#btn-add-social`, `#form-social` | redes | lista |
| `#lai-list`, `#lai_items_json`, `#btn-add-lai`, `#form-lai` | LAI | grid de cards |
| `#sleHelpModal` | `help-manual` | manual em modal `modal-xl` |
| `#reset-cms-form` + `onclick="requestSubmit()"` | `tema-cms` | dispara reset |

| Evento | Emissor | Consumidor | Payload |
|---|---|---|---|
| `sle:editor-change` | `code-editor` | `submodules/editor` | `{detail:{editor}}`; liga o `change` |
| `beforeunload` | janela | navegador | bloqueia se `sleepUnsavedFlag` |
| `show.bs.tab` | Bootstrap | editor | confirma troca de aba com `confirm()` |
| `lotep:open-gallery-picker-picker_sle_sidebar` | sidebar (click) | `gallery-picker` | `{callback(id,url,alt,mime,description)}` |
| `click` em `.sle-snippet-bar` | sidebar/asset-card | `navigator.clipboard`/fallback textarea | snippet |
| `submit` nos forms de aba | editor | limpa `sleepUnsavedFlag` | — |

AJAX:

| Chamada | Método | Resposta | Uso |
|---|---|---|---|
| `route('v2.structural-editor.assets') + '?type=css|js|image'` | GET `Accept: text/html` | fragmento de cards | sidebar carrega os 3 containers ao iniciar e após upload |
| `route('v2.structural-editor.assets.from-gallery')` | POST `FormData` + `X-CSRF-TOKEN`, `Accept: application/json` | `{success,asset,snippet}` ou `422` | cria asset da galeria e recarrega a aba `image` |
| uploads CSS/JS | POST multipart comum | `302` | formulários inline da sidebar |

### 2.5 Trechos de código pivô

```blade
{{-- V2/Modules/Support/StructuralLandingEditor/Views/cms/submodules/editor/index.blade.php:50-56 — abas do editor --}}
<ul class="nav nav-tabs" id="sleEditorTabs" role="tablist">
    <li class="nav-item" role="presentation">
        <button class="nav-link px-4 py-3 fw-bold active"
                id="sle-header-tab"
                data-bs-toggle="tab"
                data-bs-target="#tab_sle_header"
                type="button" role="tab">
            <i class="bi bi-layout-sidebar me-1"></i> Header
        </button>
    </li>
```

Os botões não declaram `aria-controls`/`aria-selected` (o Bootstrap 5 adiciona
`aria-selected` em runtime, mas não `aria-controls`); o painel tem `role="tabpanel"` e
`aria-labelledby`. Mantenha esse par ao criar uma aba nova.

```js
// V2/Modules/Support/StructuralLandingEditor/Views/cms/submodules/editor/index.blade.php:211-217 — saída da página
// beforeunload — segurança para navegação externa (F5, fechar aba)
window.addEventListener('beforeunload', function (e) {
    if (sleepUnsavedFlag) {
        e.preventDefault();
        e.returnValue = '';
    }
});
```

O navegador mostra o diálogo nativo (texto não é customizável). A flag é ligada no primeiro
`change` de qualquer editor e desligada em submit/descarte.

```js
// V2/Modules/Support/StructuralLandingEditor/Views/cms/submodules/editor/index.blade.php:229-239 — salvar e sair
var saveBtn = document.getElementById('sleUnsavedSaveBtn');
if (saveBtn) {
    saveBtn.addEventListener('click', function () {
        // Salva o formulário visível e depois navega
        var activeForm = document.querySelector('.tab-pane.active.show form');
        if (activeForm) {
            activeForm.submit();
        }
        unsavedModal.hide();
    });
}
```

Só o formulário da aba visível é salvo; as demais abas com edição pendente são perdidas.
É um comportamento a preservar (ou corrigir conscientemente) na reimplementação.

```blade
{{-- V2/Modules/Support/StructuralLandingEditor/Views/cms/components/asset-sidebar.blade.php:52-70 — lista e upload CSS --}}
<div class="sle-sbar-list" data-asset-type="css">
    <p class="text-muted small text-center py-2 mb-0">
        <i class="bi bi-inbox"></i> Carregando...
    </p>
</div>
<button class="btn btn-sm btn-outline-primary w-100 mt-1 sle-sbar-upload-toggle"
        data-target="sbar-css-upload">
    <i class="bi bi-upload"></i> Upload CSS
</button>
<div id="sbar-css-upload" class="mt-1 sle-sbar-upload-form" style="display:none;">
    <form action="{{ route('v2.structural-editor.assets.upload') }}"
          method="POST"
          enctype="multipart/form-data">
        {{ csrf_field() }}
        <input type="hidden" name="type" value="css">
        <div class="input-group input-group-sm">
            <input type="file" name="file" class="form-control form-control-sm" accept=".css" required>
            <button class="btn btn-primary btn-sm" type="submit" aria-label="Confirmar"><i class="bi bi-check"></i></button>
        </div>
    </form>
</div>
```

O estado inicial é “Carregando...”; o JS troca por cards ou “Vazio”. `accept=".css"` é a
única restrição de tipo no cliente — o servidor não valida extensão (RN-SLE-14).

```js
// V2/Modules/Support/StructuralLandingEditor/Views/cms/components/asset-sidebar.blade.php:154-174 — abrir o picker
var galleryBtn = document.getElementById('sleSbarGalleryBtn');
if (galleryBtn) {
    galleryBtn.addEventListener('click', function () {
        var checkJq = setInterval(function () {
            if (typeof jQuery !== 'undefined') {
                clearInterval(checkJq);

                var cleanName = '{{ $slePickerId }}'.replace(/\[|\]/g, '_').replace(/__+/g, '_');
                window.dispatchEvent(new CustomEvent('lotep:open-gallery-picker-' + cleanName, {
                    detail: {
                        callback: function (id, url, alt, mimeType, description) {
                            createAssetFromGallery(id, alt);
                        }
                    }
                }));
            }
        }, 100);

        setTimeout(function () { clearInterval(checkJq); }, 5000);
    });
}
```

O nome do picker é `picker_sle_sidebar`; qualquer troca de `$slePickerId` deve ser
acompanhada do `include` de `v2-service-gallery::components.gallery-picker`. O polling é
o acoplamento com jQuery; sem jQuery em 5 s o botão fica inerte e sem mensagem.

```js
// V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/scripts/links-header.blade.php:38-50 — árvore
const renderTree = (items, level, path) => {
    if (!items || items.length === 0) return null;

    const wrapper = document.createElement('div');
    wrapper.style.cssText = 'display: flex; flex-direction: column; gap: 10px; margin-top: 10px;';

    items.forEach((item, index) => {
        const currentPath = path.concat([index]);
        const pathStr = currentPath.join('-');
        if (!item.children) item.children = [];

        const itemDiv = document.createElement('div');
        itemDiv.style.cssText = 'background: #fff; border: 1px solid #ccc; padding: 10px; border-radius: 4px; border-left: 4px solid #007bff;';
```

O caminho do item é o índice unido por `-` (`0-2-1`), usado em `data-path` para mover,
indentar e remover. O nível máximo é 3 (`level < 2` para permitir indentar); itens de nível
0 com filhos têm a URL zerada (`disableUrl`).

```blade
{{-- V2/Modules/Support/StructuralLandingEditor/Views/cms/components/asset-card.blade.php:59-65 — barra de snippet --}}
<div class="mt-1 sle-snippet-bar d-flex align-items-center gap-1 bg-light border rounded px-2 py-1"
     role="button"
     class="cursor-pointer"
     title="Clique para copiar o snippet"
     data-snippet="{{ $_slSnip }}">
    <code class="flex-grow-1 text-truncate small text-muted mb-0">{{ $asset->snippet }}</code>
    <i class="bi bi-clipboard flex-shrink-0 text-primary small sle-copy-icon"></i>
</div>
```

Dois defeitos no contrato: atributo `class` duplicado (o segundo é ignorado) e
`role="button"` sem `tabindex`/tratamento de teclado — a barra só funciona com mouse.

```blade
{{-- V2/Modules/Support/StructuralLandingEditor/Views/cms/submodules/features/index.blade.php:73-78 — par hidden + checkbox --}}
<input type="hidden" name="vlibras_enabled" value="0">
<input class="form-check-input" type="checkbox" role="switch"
       id="vlibras_enabled" name="vlibras_enabled" value="1"
       style="transform: scale(1.3);"
       {{ !empty($userConfigs['vlibras_enabled']) ? 'checked' : '' }}>
```

O hidden antes do checkbox garante que “desligado” envie `0`. Repita o par para qualquer
toggle novo; sem ele, desmarcar não envia nada e o valor antigo permanece.

### 2.6 Estados

| Estado | Onde aparece | Comportamento |
|---|---|---|
| Vazio | File Library (`<p>Vazio</p>`), fragmento sem assets (`Nenhum asset X encontrado.`) | JS injeta mensagem; não há CTA |
| Carregando | “Carregando...” nos 3 containers da sidebar | substituído após o GET |
| Erro de rede | sidebar: `Erro`; galeria: `alert('Erro de conexao ao criar asset da galeria.')` | sem retry automático |
| Erro de servidor | flash `error` do controller; `422` JSON em uploads AJAX | volta para a tela anterior |
| Não autorizado | middleware/guarda: `403` ou redirect com flash | não é tratado por JS |
| Sujo (dirty) | editor | ativa `beforeunload`, intercepta links e `show.bs.tab` |
| Sem permissão de escrita | abas de layout base | botão “Adicionar” com `d-none`, botão “Salvar” oculto por `@v2can` |

## 3. Particularidades

### 3.1 O que só existe aqui

- **Detector de alterações não salvas por página**, com três caminhos: sair por link,
  fechar/recarregar e trocar de aba.
- **Construtores visuais** de árvore de menu, colunas de footer, lista de redes e grid LAI,
  todos serializando para `<textarea hidden>`.
- **Manual embutido** em modal (`help-manual` → `manual/content`) com tabela de áreas,
  componentes e variáveis; é a única documentação de usuário do módulo.
- **File Library lateral** com upload inline, cópia de snippet e picker da galeria.

### 3.2 Invariantes de UI

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-SLE-22 | Toda aba de escrita tem seu próprio `<form>` e um `tab` oculto | um submit não mistura abas | `tabs/*` e `layoutbase/*` |
| RN-SLE-23 | Construtores JS mantêm o JSON em um textarea oculto, nunca em input visível | o payload é enviado como string | `scripts/*` |
| RN-SLE-24 | `@v2can('LayoutBase','write')` esconde o botão de adicionar e o de salvar | perfil sem permissão vê a tela em leitura | `links-header.blade.php:24,33` |
| RN-SLE-25 | Toggles usam hidden `0` + checkbox `1` | desmarcar envia `0` | `features/index.blade.php:73-78` |
| RN-SLE-26 | Sidebar usa `fetch` com `Accept: text/html` e faz parse por `DOMParser` | só os cards são aproveitados; botões de excluir/copiar do card são removidos no clone | `asset-sidebar.blade.php:228-251` |
| RN-SLE-27 | `dirty` só é limpo em submit/descarte | sair pelo botão “Salvar e Sair” submete a aba ativa | `editor/index.blade.php:229-249` |

### 3.3 Armadilhas

1. **Adicionar uma área ao controller sem adicionar à UI**: `editor()` já carrega 8 áreas;
   a UI só mostra 5 abas. Uma área nova precisa de painel e de `name="areas[<area>]..."`.
2. **Mover o textarea oculto para um input visível**: o JS sobrescreve `.value` a cada
   alteração; um input sofreria validação/máscara diferente.
3. **Duplicar o ID `sleAssetPickerModal`**: o componente existe mas não é incluído; incluir
   junto com o picker da galeria cria dois caminhos concorrentes.
4. **Depender de jQuery na sidebar**: hoje há polling de 5 s; se o layout do CMS já carrega
   jQuery, o `@push` injeta uma segunda versão (3.7.1) e pode conflitar com plugins do painel.
5. **Esquecer o `role="tabpanel"`/`aria-labelledby`** ao criar aba: quebra a navegação por
   leitor de tela.
6. **Usar `asset-card` esperando exclusão**: no clone da sidebar o botão é removido; a
   exclusão só existe na resposta crua do fragmento.

### 3.4 Divergências e dívidas de UI

- `asset-picker-modal.blade.php` não é referenciado por nenhuma tela (código morto);
  a sidebar usa `v2-service-gallery::components.gallery-picker`.
- `userconfigs/temas.blade.php` e `userconfigs/components/config-history.blade.php` não são
  incluídos por nenhuma tela (legado).
- `asset-card.blade.php:59-60`: `class` duplicado.
- `asset-card.blade.php:59`: `role="button"` sem `tabindex`/tecla.
- `editor/index.blade.php:50-78`: botões de aba sem `aria-controls`/`aria-selected` estáticos.
- `manual/content.blade.php:43-51` lista “Main” como área, mas o runtime usa `main_left`/`main_right`.
- `tabs/css-head.blade.php:14-17` e `tabs/js-foot.blade.php:47-50` instruem a não usar
  `<style>`/`<script>`, enquanto o render não envolve o conteúdo e os seeders usam tags.

## 4. Integrabilidade

### 4.1 Contrato de uso das telas

| Item | Obrigatório | Tipo | Efeito |
|---|---|---|---|
| `@extends('v2-cms::layouts.app', ['title'=>..., 'subtitle'=>...])` | sim | layout | chrome do CMS |
| `@section('page-breadcrumbs')` | sim nas 5 telas | HTML | breadcrumb Dashboard → Suporte → Landing Page |
| `@section('content')` | sim | HTML | corpo |
| `@include('flash::message')` | recomendado | parcial | mensagens de sessão |
| `@include('...activity-footer')` | sim nas 5 | parcial | timeline de auditoria |
| `$contents` | só editor | `array<area, StructuralPortalContent|null>` | valores iniciais |
| `$configs` | abas layout base | `Collection<key, LayoutBaseConfig>` | payload |
| `$userConfigs` | meta/theme/features | `array` | valores |
| `$assets`/`$type` | fragmento | `Collection`, `?string` | cards |

### 4.2 Parciais e componentes

| Parcial | Props/variáveis | Slots | Consumidores | Efeito |
|---|---|---|---|---|
| `cms.components.asset-card` | `$asset`, `$showSnippet` (default true) | — | sidebar (clone), fragmento assets | card com copiar/excluir e snippet |
| `cms.components.asset-sidebar` | `$assets` opcional (não usado: sempre AJAX) | — | editor | abas CSS/JS/Img + upload + picker |
| `cms.components.asset-picker-modal` | `$filterType` (`css`/`js`/`image`/null) | — | **nenhum** (morto) | modal de seleção |
| `cms.components.help-manual` | — | — | editor | modal com `manual.content` |
| `manual.content` | — | — | help-manual | manual (áreas, componentes, variáveis, segurança, exemplos) |
| `layoutbase.components.history-table` | `$configModel`, `$logSuffix`, `$showMediaColumn` | — | abas layout base | timeline de auditoria do model |
| `userconfigs.components.config-history` | `$tabLabel`, `$monitoredKeys` | — | **nenhum** (morto) | timeline por chave de portal config |
| `scripts.links-header` | — | — | `layoutbase.links-header` | árvore de menu |
| `scripts.links-footer` | — | — | `layoutbase.links-footer` | colunas de footer |
| `scripts.redes-sociais` | — | — | `layoutbase.redes-sociais` | lista de redes |
| `scripts.transparencia-lai` | — | — | `layoutbase.transparencia-lai` | grid LAI |

### 4.3 Receitas de uso

**Adicionar uma área nova ao editor** (exemplo `banner_topo`):

1. Inclua `banner_topo` em `$areas` do controller (`SleCmsController.php:313`).
2. Crie `Views/cms/tabs/banner-topo.blade.php` com `<form>` para `editor.save`,
   `@php $v = $contents['banner_topo']->content ?? ''; @endphp`, hidden
   `areas[banner_topo][content_type]`, switch `areas[banner_topo][active]` e
   `@include('v2-cms::partials.code-editor', [...])`.
3. Inclua o partial em uma aba existente ou crie `<button data-bs-target="#tab_sle_banner">`
   + `<div class="tab-pane fade" id="tab_sle_banner" role="tabpanel" aria-labelledby="...">`.
4. Registre a área no runtime (`base.blade.php`/`portal-content.blade.php`).

Resultado: a área é salva, mas **não** renderiza até o passo 4.

**Adicionar um payload de layout base** (exemplo `aviso_topo`):

```php
// handler no LayoutBaseCmsController + case no switch
private function handleAvisoTopo(Request $request)
{
    $this->saveConfig('aviso_topo', [
        'texto' => $request->input('texto'),
        'active' => (bool) $request->input('active'),
    ]);
}
```

Na view, `<input type="hidden" name="tab" value="aviso-topo">` e o form aponta para
`v2.structural-editor.structural-data.save`. Resultado: `v2_layout_base_configs.aviso_topo`
com auditoria automática.

**Adicionar um componente ao `sle_component`**:

```php
// ComponentResolver::$map + describe()
'header-cta' => 'v2-portal::header.header-cta',
```

Resultado: `{!! sle_component('header-cta') !!}` passa a renderizar; sem a entrada em
`$descriptions`, `availableComponents()` devolve descrição vazia.

**Criar um toggle de feature**:

```blade
<input type="hidden" name="minha_feature_enabled" value="0">
<input class="form-check-input" type="checkbox" role="switch"
       id="minha_feature_enabled" name="minha_feature_enabled" value="1"
       {{ !empty($userConfigs['minha_feature_enabled']) ? 'checked' : '' }}>
```

Resultado: a chave vai a `v2_portal_configs` e fica disponível em `$portalConfigs` no portal.

### 4.4 Anti-uso

1. **Criar um `<form>` que envolva mais de uma aba.** O Bootstrap esconde painéis, mas o
   submit envia todos os campos; o JS de “salvar e sair” submete só a aba ativa e o
   comportamento fica incoerente. Mantenha um form por aba (RN-SLE-22).
2. **Chamar `assetsUpload` esperando JSON sem `Accept`.** O retorno é `302` com flash; só é
   JSON quando `$request->ajax()` ou `wantsJson()` (`SleCmsController.php:454-460`).
3. **Criar um toggle sem o hidden `0`.** Desmarcar não envia a chave e o valor antigo
   permanece (RN-SLE-25).
4. **Usar `{{ $asset->snippet }}` em um `<textarea>` ou atributo sem escape.** O snippet
   contém aspas/`{{ }}`; use `htmlspecialchars` (a sidebar usa `data-snippet` escapado) ou
   `@verbatim` no manual.

## 5. Catálogo de arquivos

| Arquivo | Papel | Variáveis | Observação |
|---|---|---|---|
| `cms/submodules/meta/index.blade.php` | tela Meta | `$userConfigs`, `$layoutConfigs` | 3 abas; forms separados |
| `cms/submodules/theme/index.blade.php` | tela Tema | `$layoutConfigs`, `$userConfigs` | reset CMS em form oculto |
| `cms/submodules/structural-data/index.blade.php` | tela Dados Estruturais | `$layoutConfigs` | 5 abas |
| `cms/submodules/features/index.blade.php` | tela Features | `$userConfigs`, `$layoutConfigs` | 4 toggles |
| `cms/submodules/editor/index.blade.php` | tela Editor | `$contents` | 5 abas, 8 áreas, modal dirty |
| `cms/submodules/assets/index.blade.php` | fragmento AJAX | `$assets`, `$type` | sem layout |
| `cms/tabs/header.blade.php` | painel header | `$contents` | CodeMirror `htmlmixed` 300px |
| `cms/tabs/main.blade.php` | painéis `main_left`+`main_right` | `$contents` | 2 editores 220px |
| `cms/tabs/footer.blade.php` | painel footer | `$contents` | 300px |
| `cms/tabs/css-head.blade.php` | painéis `css_head`+`css_foot` | `$contents` | modo `css` |
| `cms/tabs/js-foot.blade.php` | painéis `js_head`+`js_foot` | `$contents` | modo `javascript` |
| `cms/tabs/layoutbase/logos.blade.php` | logos/favicon | `$configs` | 7 media-input + favicon file |
| `cms/tabs/layoutbase/comunicado-global.blade.php` | banner | `$configs` | `datetime-local`, URL pattern |
| `cms/tabs/layoutbase/contato.blade.php` | contato | `$configs` | CEP com `data-mask="cep"` |
| `cms/tabs/layoutbase/links-header.blade.php` | menu | `$configs` | JSON em textarea |
| `cms/tabs/layoutbase/links-footer.blade.php` | colunas | `$configs` | JSON em textarea |
| `cms/tabs/layoutbase/redes-sociais.blade.php` | redes | `$configs` | JSON em textarea |
| `cms/tabs/layoutbase/transparencia-lai.blade.php` | LAI | `$configs` | default de 4 itens no PHP |
| `cms/tabs/layoutbase/components/history-table.blade.php` | timeline | `$configModel`, `$logSuffix`, `$showMediaColumn` | delega ao activitylogger |
| `cms/tabs/layoutbase/scripts/*` | JS dos construtores | `\v2_can` | IIFE, `'use strict'` |
| `cms/tabs/userconfigs/{metadados,seo,email}.blade.php` | meta | `$configs` | `box` + footer slot |
| `cms/tabs/userconfigs/{tema-cms,tema-portal}.blade.php` | temas | `$configs` | `box` |
| `cms/tabs/userconfigs/temas.blade.php` | agregado legado | `$configs` | **não referenciado** |
| `cms/tabs/userconfigs/components/config-history.blade.php` | timeline legado | `$tabLabel`, `$monitoredKeys` | **não referenciado** |
| `cms/components/asset-card.blade.php` | card | `$asset`, `$showSnippet` | 2 defeitos de a11y |
| `cms/components/asset-sidebar.blade.php` | sidebar | `$assets` opcional | jQuery 3.7.1 + picker |
| `cms/components/asset-picker-modal.blade.php` | modal | `$filterType` | **não referenciado** |
| `cms/components/help-manual.blade.php` | modal manual | — | `modal-xl` |
| `manual/content.blade.php` | manual | — | 316 linhas, exemplos com `@verbatim` |

## 6. Acessibilidade e responsividade

**O que está correto**

- Abas com `role="tablist"`/`role="tab"`/`role="tabpanel"` e `aria-labelledby`; modais com
  `aria-labelledby`, `aria-hidden`, botão de fechar com `aria-label` (“Fechar”).
- Campos com `<label for>` na maioria das abas; ícones decorativos com `aria-hidden`;
  botões só-ícone com `title`/`aria-label` (“Remover”, “Copiar”, “Confirmar”).
- Sidebar usa `input-group-sm` e `aria-label` nos botões de confirmar.
- Construtores JS declaram `aria-label`/`title` em cada input dinâmico (ex.:
  `links-header` usa “Título do Menu”, “URL do Menu”, “Desindentar…”).
- Layout responsivo por grid Bootstrap (`col-lg-9`/`col-lg-3` no editor; `col-md-*` nas abas).
  A sidebar pode ser ocultada (`sleSidebarToggle` alterna classes), mas o botão não existe
  no markup do editor atual — a função fica inativa.

**Lacunas de acessibilidade (LG-SLE-11)**

| Problema | Onde | Correção esperada |
|---|---|---|
| `.sle-snippet-bar` com `role="button"` sem `tabindex`/tecla | `asset-card.blade.php:59-62` | `button` real ou `tabindex="0"` + `keydown` |
| `class` duplicado na barra | idem | remover o segundo atributo |
| Abas do editor sem `aria-controls` | `editor/index.blade.php:50-78` | adicionar por aba |
| `confirm()` nativo na troca de aba | `editor/index.blade.php:288-296` | modal acessível |
| Contraste de `text-muted small` em fundo `bg-light` | várias abas | validar eMAG AAA |
| Construtores com `style` inline (`margin-left: level*30`) | scripts de links | indentação visual sem indicação textual do nível (há badge) |
| Estados “Carregando/Vazio/Erro” não anunciados | sidebar | `aria-live="polite"` |
| Modal do manual tem muito conteúdo e foco não é preso explicitamente | `help-manual` | depende do Bootstrap; validar |

## 7. Assets e build

- **CSS/JS próprios**: nenhum arquivo em `public/assets`; todo comportamento é inline em
  `@push('scripts')`/`@push('styles')`. Classes de estilo pontuais: `.sle-asset-sidebar`,
  `.sle-asset-card`, `.sle-snippet-bar`, `.sle-sbar-list`, `.sle-sbar-snippet`,
  `.sle-area-editor`, `.sle-editor-main`, `.sle-editor-side`, `.modal-confirm-icon`.
- **CodeMirror**: dependência do layout CMS (`v2-cms::partials.code-editor`) com `id`,
  `name`, `value`, `mode`, `height`, `label`.
- **jQuery 3.7.1** `defer` é injetado pela sidebar (`asset-sidebar.blade.php:130`) como
  fallback para o Gallery Picker.
- **Galeria**: `v2-service-gallery::components.gallery-picker` (Bootstrap modal + JS),
  `ownerModule=sle`, `allowedMimeTypes=['image/*']`, `allowShared=true`.
- **Media input**: `v2-service-media::components.media-input` nas abas de tema/logos/banner,
  com `name`, `label`, `accept`, `help`, `currentUrl`, `currentAlt`, `currentId`, `ownerModule`.
- **Cache-busting**: nenhum; o CMS depende do build do layout.
- **Sem CSP/nonce** nos scripts inline; a política do painel precisa permitir `unsafe-inline`
  ou a tela quebra (ver `01_arquitetura/09-cache-csp-minify-maintenance.md`).

## 8. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| LG-SLE-11 | Auditoria de acessibilidade (eMAG/WCAG) não foi executada; a tabela do §6 é estática | `04_ui/04-acessibilidade.md`; testes manuais | paridade de a11y não verificada |
| LG-SLE-12 | Não existe teste JS; o detector de dirty e os construtores são validados só manualmente | `tests/`, ausência de `package.json` | regressões silenciosas |
| LG-SLE-13 | `sleSidebarToggle` é referenciado no JS, mas o botão não existe no markup atual | `editor/index.blade.php`; `asset-sidebar.blade.php:205-222` | funcionalidade morta |
| LG-SLE-14 | Não foi possível confirmar se o layout CMS carrega jQuery (o que tornaria o fallback 3.7.1 uma segunda instância) | `V2/layouts/cms/**` | risco de conflito de plugins |
| LG-SLE-15 | Comportamento de `confirm()` e `beforeunload` varia por navegador; não há teste cross-browser | `editor/index.blade.php:211-217,288-296` | perda de edição não padronizada |

## 9. Checklist de paridade

- [ ] `PC-SLE-25` — As 5 telas abrem em `v2-cms::layouts.app` com breadcrumb e activity footer.
- [ ] `PC-SLE-26` — Cada aba salva apenas o próprio formulário; o payload contém o `tab` correto.
- [ ] `PC-SLE-27` — Editar qualquer CodeMirror arma o `beforeunload`; submeter limpa a flag.
- [ ] `PC-SLE-28` — Link interno com edição pendente abre `#sleUnsavedModal`; “Descartar e Sair” navega; “Salvar e Sair” submete a aba ativa.
- [ ] `PC-SLE-29` — Trocar de aba com edição pendente dispara `confirm()` e cancela a troca em “Cancelar”.
- [ ] `PC-SLE-30` — Sidebar carrega CSS/JS/Img via AJAX e mostra “Vazio” quando não há cards.
- [ ] `PC-SLE-31` — “Selecionar da Galeria” abre o picker `picker_sle_sidebar` e cria asset via `from-gallery`.
- [ ] `PC-SLE-32` — Clique no snippet copia para o clipboard (com fallback para `document.execCommand`).
- [ ] `PC-SLE-33` — Construtor de links permite 3 níveis, reordena, indenta/desindenta e serializa em `header_links_json`.
- [ ] `PC-SLE-34` — Construtor de footer cria/remove colunas e links; JSON em `footer_links_json`.
- [ ] `PC-SLE-35` — Construtor LAI edita nome/ícone/URL/ativo, reordena e normaliza `url` com `/`.
- [ ] `PC-SLE-36` — Toggles de features enviam `0` quando desmarcados.
- [ ] `PC-SLE-37` — Abas de layout base escondem “Adicionar”/“Salvar” sem `LayoutBase.write`.
- [ ] `PC-SLE-38` — Modal “Manual” abre com as 11 entradas de `sle_component` e a tabela de variáveis.

## 10. Evidências consultadas

- `V2/Modules/Support/StructuralLandingEditor/Views/cms/submodules/{meta,theme,structural-data,features,editor,assets}/index.blade.php`
- `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/{header,main,footer,css-head,js-foot}.blade.php`
- `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/{logos,comunicado-global,contato,links-header,links-footer,redes-sociais,transparencia-lai}.blade.php`
- `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/scripts/{links-header,links-footer,redes-sociais,transparencia-lai}.blade.php`
- `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/userconfigs/{metadados,seo,email,tema-cms,tema-portal,temas}.blade.php`
- `V2/Modules/Support/StructuralLandingEditor/Views/cms/components/{asset-card,asset-sidebar,asset-picker-modal,help-manual}.blade.php`
- `V2/Modules/Support/StructuralLandingEditor/Views/manual/content.blade.php:1-316`
- `V2/Modules/Support/StructuralLandingEditor/Routes/cms.php:17-62`; `Controllers/SleCmsController.php:308-528`
