---
id: SVC-LANDINGSECTIONSEDITOR
tipo: servico
onda: 2
status: concluido
unidades: [SVC-LANDINGSECTIONSEDITOR]
fontes:
  - V2/Services/LandingSectionsEditor/Views/components/
  - V2/Services/LandingSectionsEditor/Views/preview.blade.php
  - V2/Services/LandingSectionsEditor/Views/forms/
depende_de: [SVC-CONTENTRICHEDITOR, SVC-GALLERY]
consumidores: [MOD-LAYOUTS-HOMEPAGE, MOD-INSTITUCIONAL-SOBRENOS]
criticidade: alta
dono: SvcLse
---

# Contrato de UI — LandingSectionsEditor

Cobre `Views/components/**`, `Views/preview.blade.php` e o contrato JS/DOM consumido pelos
formulários de seção. O catálogo de tipos está em `widgets-e-schemas.md`; o núcleo lógico em
`spec.md`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Entrega um editor de duas colunas (lista de seções + iframe de preview) e um viewer que transforma o array de seções em HTML, com modal próprio de configuração e seleção de mídia via Gallery. |
| Entrada | Editor: `name`, `value`, `label`. Viewer: `sections`. HTTP: rotas do serviço. Eventos: cliques delegados, `lotep:open-gallery-picker-lse_global_media_picker`, `message`. |
| Saída | DOM com IDs derivados de `name`; `<input type="hidden" name="<name>">` com JSON; iframe de preview; HTML de seção; `postMessage` `lotep:update-section-html`. |
| Efeitos colaterais | `@push('scripts')`, `@push('css')`, `@push('head-styles')`; `formTemplatesCache` global; POSTs para `/validate`, `/preview/save`, `/preview/render-section`; abertura do `gallery-picker` global. |
| Particularidade central | Modal é **custom** (div + overlay), não Bootstrap; largura `modal-xl` para `interactive-table` é aplicada a um `.modal-dialog` que não existe. A seleção de mídia é delegada ao componente `gallery-picker` do Gallery, com callback. |
| Como se integra | `@include('v2-service-landingsectionseditor::components.forms.landing-sections-editor', ['name' => ..., 'value' => ..., 'label' => ...])` dentro de um `<form>` do consumidor; `@include('...components.landing-sections-viewer', ['sections' => ...])` no portal. |
| Se removido, quebra | Editores de Homepage/Sobre Nós, preview e páginas públicas das duas páginas. |
| Superfície pública | 2 componentes (`landing-sections-editor`, `landing-sections-viewer`), 1 subcomponente (`forms.image-picker`), 5 partials `logic/*`, 1 view de página (`preview`). |

## 1. Efeito

### 1.1 Transformação que a UI realiza

- Editor: sidebar com dropdown “ADICIONAR SEÇÃO”, lista reordenável (setas ↑/↓), botões editar/remover e um iframe à direita. O estado é um array JS; o valor persistível é o JSON do `<input type="hidden">`.
- Modal: painel fixo com header “Configurar Seção”, body com o HTML do form carregado por AJAX e footer “Fechar”/“Salvar Seção”.
- Viewer: percorre `sections`, resolve widget e inclui `render.<type>` dentro de `div[data-section-id]`.
- Preview: página completa do portal (layout `v2-portal::app-layout`, sem header/footer principais) que renderiza o viewer com as seções do cache.

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Atualiza o hidden com JSON | DOM | `persistState` | `Views/components/forms/logic/preview-manager.blade.php:5-7` |
| Cacheia HTML de form por `new_<type>`/`edit_<id>` | memória JS | `loadForm` | `Views/components/forms/logic/sections-manager.blade.php:178,194-206` |
| Envia seções sanitizadas ao cache | HTTP/cache | `triggerPreviewSave` | `preview-manager.blade.php:17-31` |
| Recarrega o iframe | DOM | `refreshPreview` | `preview-manager.blade.php:36-41` |
| Troca HTML de uma seção via `postMessage` | DOM do iframe | `saveSection` | `sections-manager.blade.php:302-317` |
| Abre o seletor global de mídia | Gallery | `registerEvents` | `sections-manager.blade.php:121-132` |
| Aplica valor do picker no input/thumbnail | DOM | `selectImage` | `media-manager.blade.php:4-15` |
| Renderiza HTML de seção no portal | HTML | viewer | `Views/components/landing-sections-viewer.blade.php:33-48` |

### 1.3 Contrato observável

| Ação | Resultado |
|---|---|
| Incluir o editor sem `name` ou sem `value` | Erro Blade por variável indefinida (`$value` em `landing-sections-editor.blade.php:3`; IDs com `name` vazio) |
| Incluir o editor com `value = []` ou `null` | Hidden recebe `[]` (`:7-9`) |
| Clicar em “ADICIONAR SEÇÃO” → item | `currentType = data-type`, `currentSectionId = ''`, `loadForm()`; overlay e painel recebem `.show` (`sections-manager.blade.php:52-55,14-21`) |
| Clicar em editar | `currentSectionId = data-id`, parâmetros vindos de `sections` e `loadForm(...)` (`:57-68`) |
| Clicar em ↑/↓ | Troca posições, recalcula `order`, `persistState()` (`:71-97`) |
| Clicar em remover | `confirm('Deseja realmente remover esta secao?')`; remove e `persistState()` (`:336-352`) |
| Salvar inválido | `alert('Por favor, corrija os seguintes erros:\n- <campo>: <msgs>')`; caso contrário `alert('Erro na validacao dos dados da secao.')` (`:319-332`) |
| Salvar válido | Toast `'Secao atualizada localmente!'` (se `toastr`), modal fecha, estado persiste e preview é atualizado (`:296-317`) |
| Clicar no overlay/X/Fechar | `hideSectionModal()` (`:107-117`) |
| Selecionar imagem no Gallery | `selectImage(id,url)`: input recebe id, texto “Imagem Selecionada (ID: <id>)”, thumbnail e botão limpar habilita (`media-manager.blade.php:4-15`) |
| Limpar imagem | Input vazio, texto “Nenhuma imagem selecionada”, thumbnail vazio, botão desabilita (`sections-manager.blade.php:134-141`) |

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

- Não há slot: só props e `@push`.
- Não há drag-and-drop: `initSortable()` e `updateOrderFromDOM()` são no-ops (`sections-manager.blade.php:147-158`).
- Não há focus trap, fechamento por `Esc` ou retorno de foco no modal (nenhum handler de `keydown`).
- Não há upload/edição de mídia no fluxo vivo do editor: o `gallery-picker` do Gallery usa as rotas dele; as rotas `/media` do LSE só são chamadas pelo partial morto `image-picker-modal.blade.php:76-77,84,129`.
- Não há validação client-side além do `required` do HTML dos forms: a autoridade é o `/validate`.
- Não há teardown: listeners são globais e permanecem após remoção do DOM.

## 2. Abordagem

### 2.1 Estratégia e por quê

Um único componente Blade concentra marcação e JS inline, com partials `logic/*` incluídos **dentro do mesmo `<script>`** para dividir o código sem criar módulos ES. O escopo é isolado por IIFE (`landing-sections-editor.blade.php:114-155`), mas o estado de cache e o helper de rich text ficam em `window`/escopo global. O modal é custom para não depender do backdrop do Bootstrap 5, que estava deixando `modal-open` e `padding-right` residuais (`section-modal.blade.php:1`; `sections-manager.blade.php:401-406`).

### 2.2 Mecânica passo a passo

1. Render: calcula token e serializa `value` (`landing-sections-editor.blade.php:1-12`), inclui assets do ContentRichEditor (`:14`), estilos (`:16`), hidden (`:24`), sidebar com dropdown (`:26-81`), iframe (`:84-86`), modal (`:90`), gallery-picker (`:92`) e registra o script (`:94-158`).
2. Init do IIFE: constantes `name`, `previewToken`, `csrfToken`, mapa `urls` (`:115-128`), catálogo `sectionTypesData` (`:131`), estado (`:134-138`), load inicial (`:141`), métodos (`:144-150`) e `init()` (`:153`).
3. `init()`: limpa backdrops, registra eventos, desenha sidebar, grava o hidden e faz `POST /preview/save` seguido de `refreshPreview()` em sucesso **ou** erro (`sections-manager.blade.php:401-424`).
4. Interação: eventos delegados em `document` para itens, setas, editar, remover, imagens; eventos diretos para o dropdown (`:52-55`) e botões do modal (`:105-112`).
5. Salvar: serializa, valida, reconstrói `parameters`, atualiza `sections`, fecha o modal, `persistState()`, toast e `POST /render-section` para swap (`:215-317`).
6. Preview: debounce de 800 ms → `POST /preview/save` → troca `src` do iframe (`preview-manager.blade.php:5-41`).

### 2.3 Trechos de código pivô

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:1-11 --}}
@php
    $token = \Illuminate\Support\Str::random(40);
    $initialValue = $value;
    if (is_array($value)) {
        $initialValue = json_encode($value);
    }
    if (empty($initialValue) || $initialValue === 'null') {
        $initialValue = '[]';
    }

    $sectionTypes = \V2\Services\LandingSectionsEditor\Services\LandingSectionsEditorRendererService::getSectionTypes();
@endphp
```

**Efeito.** Define as três props reais: `value` (array ou string JSON), `name` (usado adiante) e `label`; o catálogo é resolvido no servidor e também injetado no JS (`:131`).

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:18-28 --}}
<div class="mb-3 position-relative">
    @if(!empty($label))
        <label for="landing-sections-hidden-{{ $name }}" class="form-label fw-bold">{{ $label }}</label>
    @endif

    {{-- Input hidden principal que conterá o JSON serializado das seções --}}
    <input type="hidden" name="{{ $name }}" id="landing-sections-hidden-{{ $name }}" value="{{ $initialValue }}">

    <div class="lse-container-{{ $name }}">
        <!-- Painel Esquerdo: Lista de Seções -->
        <div class="lse-sidebar-{{ $name }}">
```

**Efeito.** O valor persistido é um único campo oculto com o nome escolhido pelo consumidor. Os elementos estruturais do editor (container, sidebar, preview, lista, iframe, overlay e painel do modal) usam `$name` como sufixo de classe/ID, o que permite (teoricamente) mais de uma instância.

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/logic/section-modal.blade.php:1-17 --}}
{{-- OVERLAY + PAINEL CUSTOM — sem Bootstrap modal, sem backdrop gerenciado pelo Bootstrap --}}
<div class="lse-overlay" id="lse-overlay-{{ $name }}"></div>

<div class="lse-modal-panel" id="section-modal-{{ $name }}" role="dialog" aria-modal="true" aria-labelledby="section-modal-title-{{ $name }}">
    <div class="lse-modal-header">
        <h5 class="lse-modal-title" id="section-modal-title-{{ $name }}">Configurar Seção</h5>
        <button type="button" class="lse-modal-close" id="btn-modal-close-{{ $name }}" aria-label="Fechar">&times;</button>
    </div>
    <div class="lse-modal-body" id="modal-body-content-{{ $name }}">
        <div class="text-center py-4">
            <span class="spinner-border text-primary" role="status" aria-hidden="true"></span>
        </div>
    </div>
    <div class="lse-modal-footer">
        <button type="button" class="btn btn-outline-secondary fw-bold lse-modal-dismiss" id="btn-modal-close2-{{ $name }}">Fechar</button>
        <button type="button" class="btn btn-primary fw-bold" id="btn-modal-save-{{ $name }}">Salvar Seção</button>
    </div>
</div>
```

**Efeito.** O modal tem semântica ARIA básica (`role=dialog`, `aria-modal`, `aria-labelledby`) e dois botões de fechar; o body começa com spinner e é substituído pelo form. Não há `.modal-dialog`, então `$('#section-modal-'+name+' .modal-dialog')` é vazio (`sections-manager.blade.php:163`).

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:161-178 --}}
const loadForm = (type, sectionId, parameters = {}) => {
    const $modalBody = $('#modal-body-content-' + name);
    const $modalDialog = $('#section-modal-' + name + ' .modal-dialog');

    $modalBody.html('<div class="text-center py-4"><span class="spinner-border text-primary" role="status"></span><p class="text-muted mt-2">Carregando formulario...</p></div>');

    // Ajuste de largura para tabelas interativas
    if (type === 'interactive-table') {
        $modalDialog.addClass('modal-xl');
    } else {
        $modalDialog.removeClass('modal-xl');
    }

    // Abre o modal com estado limpo — instancia nova, sem backdrops acumulados
    showSectionModal();

    // Chave unica de cache
    const cacheKey = sectionId ? 'edit_' + sectionId : 'new_' + type;
```

**Efeito.** O form é buscado em `GET /form` com `type`, `section_id` e `parameters` (JSON) (`:200-206`). O ajuste de largura é inócuo no DOM atual; a chave de cache distingue edição de criação, mas não o `name` da instância.

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:280-294 --}}
if (currentSectionId) {
    // Edicao de secao existente
    const found = sections.find(s => s.id === currentSectionId);
    if (found) {
        found.parameters = parameters;
    }
} else {
    // Nova secao
    sections.push({
        id: savedSectionId,
        type: currentType,
        parameters: parameters,
        order: sections.length + 1
    });
}
```

**Efeito.** O array mantém apenas `id`, `type`, `parameters`, `order`; qualquer metadado extra vindo do payload é preservado em edição (o objeto `found` permanece), mas uma seção nova nasce só com esses quatro campos.

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:121-132 --}}
$(document).on('click', '.lse-container-{{ $name }} .btn-select-image, #section-modal-' + name + ' .btn-select-image', function() {
    activeImageInputId = $(this).data('input-id');

    window.dispatchEvent(new CustomEvent('lotep:open-gallery-picker-lse_global_media_picker', {
        detail: {
            callback: (id, url) => {
                selectImage(id, url);
            }
        }
    }));
});
```

**Efeito.** O editor não implementa picker: apenas guarda `data-input-id` e delega ao `gallery-picker` incluído com `name => 'lse_global_media_picker'` (`landing-sections-editor.blade.php:92`). O Gallery escuta `lotep:open-gallery-picker-<pickerId>` (`V2/Services/Gallery/Views/components/logic/scripts/main-orchestrator.blade.php:61`).

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/image-picker.blade.php:12-28 --}}
<div class="image-picker-container" data-input-id="{{ $inputId }}">
    <label class="form-label fw-bold">{{ $label }}</label>
    <div class="input-group">
        <input type="hidden" name="{{ $name }}" value="{{ $value }}" id="{{ $inputId }}">
        <input type="text" class="form-control" id="{{ $inputId }}-preview-text" value="{{ $value ? 'Imagem Selecionada (ID: '.$value.')' : 'Nenhuma imagem selecionada' }}" readonly>
        <button class="btn btn-primary btn-select-image d-inline-flex align-items-center gap-1 fw-bold" type="button" data-input-id="{{ $inputId }}">
            <i class="bi bi-image"></i> Selecionar
        </button>
        <button class="btn btn-danger btn-clear-image d-inline-flex align-items-center justify-content-center" type="button" data-input-id="{{ $inputId }}" {{ !$value ? 'disabled' : '' }}>
            <i class="bi bi-x-lg"></i>
        </button>
    </div>
    <div class="image-preview-thumbnail" id="{{ $inputId }}-thumbnail" style="margin-top: 10px;">
        @if($thumbnailUrl)
            <img src="{{ $thumbnailUrl }}" class="img-thumbnail" style="max-height: 80px; margin-top: 5px;" alt="Prévia da imagem selecionada">
        @endif
    </div>
</div>
```

**Efeito.** O contrato do subcomponente é `inputId` (obrigatório; base de 3 IDs), `name` (campo do form, ex.: `parameters[image_id]`), `value` (id numérico) e `label`. O thumbnail é resolvido no servidor via `StorageService::url` (`:4-9`).

```blade
{{-- V2/Services/LandingSectionsEditor/Views/preview.blade.php:20-37 --}}
@push('scripts')
<script>
window.addEventListener('message', function(e) {
    if (e.data && e.data.type === 'lotep:update-section-html') {
        const sectionId = e.data.sectionId;
        const html = e.data.html;
        const block = document.querySelector('.lse-render-section-block[data-section-id="' + sectionId + '"]');
        if (block) {
            // Swap inner HTML
            block.innerHTML = html;
        } else {
            // Fallback: full reload if section is new or not found
            window.location.reload();
        }
    }
});
</script>
@endpush
```

**Efeito.** O iframe escuta o postMessage do editor e substitui o bloco; se a seção é nova (id ainda não renderizado no iframe) ou o id não existe, a página inteira recarrega. Não há validação de origem (`'*'` no envio).

```blade
{{-- V2/Services/LandingSectionsEditor/Views/components/forms/logic/styles.blade.php:122-140 --}}
.lse-modal-panel {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 1030;
    display: none;
    flex-direction: column;
    background: #fff;
    border-radius: 8px;
    width: 800px;
    max-width: 90vw;
    max-height: 85vh;
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.lse-modal-panel.show {
    display: flex;
    flex-direction: column;
}
```

**Efeito.** Header/footer fixos e body com scroll dependem de `flex-direction: column`, `flex-shrink: 0` e `.lse-modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }` (`:142-175`). Há teste unitário garantindo esse contrato (`tests/Unit/V2/Services/LandingSectionsEditor/LseModalStylesTest.php:18-46`).

### 2.4 Algoritmos e custos

- `renderSidebar()` reconstrói todo o HTML da lista a cada `persistState()` (`sections-manager.blade.php:356-388`): O(n) por edição, com recriação de listeners delegados (os listeners ficam em `document`, então não vazam por item).
- `saveSection()` percorre todos os campos do form duas vezes: `serialize()` para validar e `serializeArray()` para montar o objeto (`:217,223-273`).
- `loadForm()` usa cache local, mas o cache é invalidado a cada save da seção (`:275-278`).
- Preview: 1 POST por rajada de 800 ms + 1 POST `/render-section` por save; o iframe é recarregado por completo na sincronização e recebe swap parcial no save.

## 3. Particularidades

### 3.1 O que é exclusivo deste componente

- Modal custom com contador `lseOverlayCount` para sobreviver a sub-modais (`sections-manager.blade.php:9-35`) — o Gallery usa Bootstrap Modal, o LSE não.
- ID baseado em `name` em **todos** os elementos (`lse-*-<name>`), o que torna o componente reutilizável em tese, mas o cache JS é global.
- Fluxo de preview duplo: recarga total (debounce) + swap parcial (`postMessage`).
- Serialização própria de `parameters[a][b][]` para objeto/array (`sections-manager.blade.php:223-273`).

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-LSE-16 | O nome do campo persistido é exatamente a prop `name`. | O consumidor precisa ler `landing_sections_json` no submit. | `landing-sections-editor.blade.php:24`; `V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:52` |
| RN-LSE-17 | O estado só é válido se `sections` for array. | `state-loader` faz `JSON.parse` e cai em `[]` no erro. | `state-loader.blade.php:2-9` |
| RN-LSE-18 | O modal não usa Bootstrap; `.modal-dialog`/`.modal-xl` não existem no DOM. | Ajuste de largura de `interactive-table` não tem efeito. | `section-modal.blade.php:4-17`; `sections-manager.blade.php:163-172` |
| RN-LSE-19 | Toda seleção de imagem passa por `activeImageInputId` + callback do Gallery. | Dois cliques concorrentes sobrescrevem o alvo. | `sections-manager.blade.php:122-131`; `media-manager.blade.php:4-5` |
| RN-LSE-20 | O preview é sempre recarregado após o primeiro save, mesmo em erro de AJAX. | `init()` chama `refreshPreview()` em `success` e `error`. | `sections-manager.blade.php:414-423` |
| RN-LSE-21 | `postMessage` usa origem `'*'` e não valida `event.origin`. | Qualquer página que consiga postar no iframe pode trocar HTML. | `sections-manager.blade.php:310-314`; `preview.blade.php:22-25` |
| RN-LSE-22 | Remover/editar dependem de `id` presente em cada seção. | Seção sem id não é editável nem removível via `findIndex`. | `sections-manager.blade.php:60-68,74-76,101-103,336-346` |

### 3.3 Casos-limite

| Caso | Comportamento | Fonte |
|---|---|---|
| `value` inválido (string não-JSON) | `JSON.parse` falha, loga `[LandingSectionsEditor] Failed to parse initial value:` e assume `[]`. | `state-loader.blade.php:6-8` |
| Lista vazia | Sidebar mostra “Nenhuma secao adicionada.” | `sections-manager.blade.php:358-361` |
| Tipo desconhecido no estado | Sidebar usa `{icon:'bi bi-box', title: section.type}` | `sections-manager.blade.php:366` |
| Form falha ao carregar | Body do modal mostra `Erro ao carregar o formulario da secao.` | `:207-209` |
| Validação 422 | `alert` com cada erro concatenado; linha por campo. | `:319-327` |
| Validação 500/outro | `alert('Erro na validacao dos dados da secao.')` | `:329-331` |
| Remover último item | Setas desabilitadas (`isFirst`/`isLast`). | `:367-368,378-379` |
| Duas instâncias do editor | IDs não colidem, mas `formTemplatesCache` é compartilhado e o helper `window.reindexTiptapWrapper` é sobrescrito. | `:3`; `landing-sections-editor.blade.php:97-111` |
| Modal aberto e clique no overlay | `hideSectionModal()` decrementa o contador e esconde. | `:24-35` |
| `toastr` ausente | Nenhuma notificação de sucesso; operação continua. | `:300,350` |
| `bootstrap` ausente (Gallery) | Abertura do picker falha; seleção de imagem indisponível. | `V2/Services/Gallery/Views/components/logic/scripts/main-orchestrator.blade.php:7-12` |

### 3.4 Armadilhas de reimplementação

1. **Trocar o modal custom por Bootstrap Modal**: o CSS/teste e o contador `lseOverlayCount` deixam de valer; o comportamento de backdrop residual era justamente o motivo do modal custom (`section-modal.blade.php:1`).
2. **Não incluir o `<input type="hidden">` dentro do `<form>` do consumidor**: o dado nunca é submetido (`landing-sections-editor.blade.php:24`; consumidores em `Views/cms/editor.blade.php`).
3. **Esquecer `@stack('scripts')`, `@stack('css')`, `@stack('head-styles')` no layout**: o editor não inicializa e o CSS do modal não é aplicado (`@push` em `landing-sections-editor.blade.php:94`, `styles.blade.php:1`, `landing-sections-viewer.blade.php:16`).
4. **Assumir que `sections` é reordenado por `order`**: o JS usa a ordem do array e reescreve `order`; o render público nem lê `order`.
5. **Usar `data-input-id` sem o par `<inputId>`/`<inputId>-preview-text`/`<inputId>-thumbnail`**: `selectImage` falha silenciosamente nos seletores vazios (`media-manager.blade.php:5-14`).
6. **Contar com o picker local**: `image-picker-modal.blade.php` escuta `lotep:open-global-media-picker`, evento que o editor não emite; o fluxo vivo é o Gallery.

### 3.5 Dívidas e divergências

| Item | Evidência | Impacto |
|---|---|---|
| `.modal-dialog`/`.modal-xl` inexistentes no painel custom. | `sections-manager.blade.php:163-172`; `section-modal.blade.php:4-17` | Largura de `interactive-table` nunca é ampliada. |
| `media-manager.blade.php` chama `selectImage` mas não abre picker; `sections-manager` define outro `selectImage`? | `media-manager.blade.php:4`; `sections-manager.blade.php:121-132` | O `selectImage` do IIFE é o de `media-manager`; o callback do Gallery o usa. OK, mas frágil: uma segunda definição no mesmo escopo quebraria. |
| `image-picker-modal` morto e com lógica duplicada de galeria/upload/rotação. | `image-picker-modal.blade.php:1-303` | Manutenção duplicada; rotas `/media` sem consumidor vivo. |
| O editor define `urls.mediaList`/`urls.mediaStore` sem uso. | `landing-sections-editor.blade.php:125-126` | Código morto; confunde integração. |
| `html-free` posta `resize-iframe`, preview não escuta. | `Views/render/html-free.blade.php:44-47`; `preview.blade.php:22-34` | Altura automática não funciona. |
| Sem `Esc`/focus trap. | `sections-manager.blade.php:38-142` | A11y incompleta (ver §5). |
| `confirm`/`alert` nativos. | `:319-333,337` | Sem i18n/estilo; bloqueia a thread. |

## 4. Integrabilidade

### 4.1 API pública

| Componente | Props | Obrigatória | Tipo | Efeito |
|---|---|---|---|---|
| `components.forms.landing-sections-editor` | `name` | sim | string | Nome do hidden e sufixo de todos os IDs/classes |
| | `value` | sim | array\|string JSON\|null | Estado inicial; `null`/vazio → `[]` |
| | `label` | não | string | Rótulo do hidden; sem ele não há `<label>` |
| `components.landing-sections-viewer` | `sections` | sim | array\|string JSON | Lista a renderizar; string é decodificada; inválida → `[]` |
| `components.forms.image-picker` | `inputId` | sim | string | Base dos IDs (`<inputId>`, `-preview-text`, `-thumbnail`) |
| | `name` | sim | string | Nome do campo, ex.: `parameters[image_id]` |
| | `value` | não | int\|string | ID da mídia; habilita/desabilita limpar |
| | `label` | não | string | Texto do rótulo |

### 4.2 Componentes Blade e contrato de UI

**Editor — estrutura e IDs**

| Elemento | ID/classe | Fonte |
|---|---|---|
| Hidden | `#landing-sections-hidden-<name>` (`name="<name>"`) | `landing-sections-editor.blade.php:24` |
| Container | `.lse-container-<name>` | `:26` |
| Sidebar / preview | `.lse-sidebar-<name>` / `.lse-preview-<name>` | `:28,84` |
| Dropdown de adicionar | `.btn-add-section-<name>` com `data-type` | `:55,65` |
| Lista | `#section-list-<name>` | `:78` |
| Iframe | `#preview-iframe-<name>` | `:85` |
| Overlay/painel | `#lse-overlay-<name>` / `#section-modal-<name>` | `section-modal.blade.php:2,4` |
| Título/body/footer | `#section-modal-title-<name>`, `#modal-body-content-<name>`, `#btn-modal-save-<name>` | `:6,9,16` |
| Fechar | `#btn-modal-close-<name>`, `#btn-modal-close2-<name>` | `:7,15` |
| Item de seção | `.lse-section-item-<name>` com `data-id` e `data-type` | `sections-manager.blade.php:370` |
| Ações do item | `.btn-move-up-section`, `.btn-move-down-section`, `.btn-edit-section`, `.btn-remove-section` | `:378-382` |
| Imagem | `.image-picker-container`, `.btn-select-image[data-input-id]`, `.btn-clear-image[data-input-id]` | `image-picker.blade.php:12,17,20` |

**Viewer**

| Elemento | Contrato | Fonte |
|---|---|---|
| Bloco de seção | `div.lse-render-section-block[data-section-id="<id>"]` | `landing-sections-viewer.blade.php:41` |
| Render | `@include($widget->getRenderView() ?: '...render.<type>', ['section'=>..., 'data'=>...])` | `:37-45` |
| Estilos de alto contraste | `.acessibilidade-alto-contraste .blog-entry/.bg-light/.bg-white/.text-muted` | `:16-31` |
| Vazio | Nenhum HTML quando `sections` é vazio (sem mensagem) | `:33` |

### 4.3 JS e ciclo de vida

| Fase | O que acontece | Fonte |
|---|---|---|
| Load | `$(document).ready` define `window.reindexTiptapWrapper` e executa o IIFE | `landing-sections-editor.blade.php:96-155` |
| Estado | `sections`, `currentType`, `currentSectionId`, `activeImageInputId`, `previewDebounceTimer` | `:134-138` |
| Init | `init()` limpa backdrops, registra eventos, desenha sidebar, grava hidden e abre o preview | `sections-manager.blade.php:401-424` |
| CSRF | `$.ajaxSetup({headers:{'X-CSRF-TOKEN': csrfToken}})` | `:40-42` |
| Eventos | `change` em `.icon-selector`; `click` delegado para adicionar/editar/mover/remover/salvar/fechar/imagem; `message` no preview | `:38-142`; `preview.blade.php:22` |
| Emite | `CustomEvent('lotep:open-gallery-picker-lse_global_media_picker', {detail:{callback}})`; `postMessage({type:'lotep:update-section-html', sectionId, html}, '*')` | `:125-131,310-314` |
| Escuta | Nenhum CustomEvent próprio; apenas `message` no preview | `preview.blade.php:22` |
| Teardown | Inexistente: listeners em `document` e cache global permanecem | `:3,38-142` |
| Múltiplas instâncias | Escopos JS separados por IIFE, mas cache/helper globais | `landing-sections-editor.blade.php:97,114` |
| Sem JS | Sidebar vazia; hidden mantém o JSON inicial; submit salva sem alterações | `:78-80,24` |

Dependências: jQuery; Bootstrap 5 (dropdown `data-bs-toggle` no editor e modal do Gallery); `UIManager` e `initContentRichEditors` (guarda `typeof`); `toastr` (guarda); `crypto.getRandomValues`.

### 4.4 Ordem de inicialização e registro

1. Layout do CMS deve renderizar `@stack('css')`, `@stack('head-styles')` e `@stack('scripts')`; o editor faz `@push` nesses três stacks (`styles.blade.php:1`; `landing-sections-viewer.blade.php:16`; `landing-sections-editor.blade.php:94`).
2. O editor inclui `content-rich-editor` com `assets_only` **antes** do markup (`:14`), garantindo `window.initContentRichEditors` antes de `loadForm`.
3. O editor inclui `gallery-picker` com `name => 'lse_global_media_picker'` (`:92`); o Gallery registra o listener no load e depende de Bootstrap (`main-orchestrator.blade.php:61`).
4. O hidden deve estar dentro do `<form>` do consumidor (`V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:46-56`).
5. O iframe só funciona se as rotas `v2.landing-sections.*` estiverem registradas (`V2/Core/Providers/V2ServiceProvider.php:203-214`).

### 4.5 Receitas de uso

**1. Editor mínimo no CMS.**

```blade
<form action="{{ route('v2.homepage.sections.save') }}" method="POST">
    {{ csrf_field() }}
    @include('v2-service-landingsectionseditor::components.forms.landing-sections-editor', [
        'name' => 'landing_sections_json',
        'value' => $sections,
        'label' => 'Seções da Homepage',
    ])
</form>
```

Resultado: lista + preview; no submit, `landing_sections_json` contém o JSON atualizado.

**2. Viewer no portal.**

```blade
@include('v2-service-landingsectionseditor::components.landing-sections-viewer', [
    'sections' => $sections,
])
```

Resultado: blocos `div.lse-render-section-block[data-section-id]`; tipos sem view são omitidos.

**3. Campo de imagem dentro de um form de seção.**

```blade
@include('v2-service-landingsectionseditor::components.forms.image-picker', [
    'label' => 'Imagem de Fundo',
    'name' => 'parameters[background_image_id]',
    'value' => $data['background_image_id'] ?? '',
    'inputId' => 'hero-bg-image',
])
```

Resultado: hidden com o id, texto de prévia, botão “Selecionar” que abre o picker global e botão limpar.

**4. Swap programático do preview (fora do editor).**

```js
iframe.contentWindow.postMessage({
    type: 'lotep:update-section-html',
    sectionId: '3f2b6f0e-7c1a-4a4e-9d0f-6a1c2b3d4e5f',
    html: '<section>...</section>'
}, '*');
```

Resultado: substitui o bloco correspondente; se não existir, recarrega a página do preview.

### 4.6 Anti-uso

1. **Colocar o editor fora do `<form>`**: o hidden não é enviado e o consumidor recebe `landing_sections_json` ausente (o `validate` do controller exige `required|string`, `HomepageCmsController.php:55-57`).
2. **Renderizar o viewer sem `prepareSections()`**: algumas views de módulo dependem de `image`/`*_variants`; o viewer chama `prepareSections()` internamente (`landing-sections-viewer.blade.php:12-13`), mas controllers que montam HTML manualmente sem passar pelo viewer precisam chamar antes.
3. **Contar com o modal do LSE para upload**: o editor vivo só escolhe mídia existente; upload é feito no modal do Gallery (`V2/Services/Gallery/Views/components/gallery-picker.blade.php`).

### 4.7 Custos e limites

- `renderSidebar` recria todo o HTML a cada mudança; listas muito grandes custam O(n) por interação.
- Cada instância consome 1 token de 40 chars e 1 entrada de cache de 1 h no servidor.
- O editor mantém todos os forms visitados em memória (`formTemplatesCache`), sem limite.
- O iframe recarrega a página inteira do preview a cada rajada de edição; em páginas grandes, o custo é o do portal completo.
- Sem debounce na validação/salvamento manual: cada clique em “Salvar Seção” faz 2 POSTs (validate + render-section).

## 5. Acessibilidade

| Item | Estado | Fonte |
|---|---|---|
| Modal com `role="dialog"`, `aria-modal="true"`, `aria-labelledby` | presente | `section-modal.blade.php:4-6` |
| Botão X com `aria-label="Fechar"` | presente | `:7` |
| Spinner com `role="status"` | presente (mas `aria-hidden="true"` no estado inicial) | `:11` |
| Iframe com `title="Visualização em tempo real"` | presente | `landing-sections-editor.blade.php:85` |
| Labels dos forms | presentes via `Form::label(..., ['for' => ...])` nos campos principais | `Views/forms/hero.blade.php:2-3` |
| Cores | inputs `type="color"` com `aria-label` | `Views/forms/hero.blade.php:26,37` |
| Focus trap / foco inicial / retorno de foco | ausente | `sections-manager.blade.php:14-35` |
| Fechar com `Esc` | ausente (nenhum `keydown`) | `:38-142` |
| Itens da galeria clicáveis por teclado | ausente (cards `div` com `onclick` no Gallery) | `image-picker-modal.blade.php:92-116` |
| Contraste | depende dos renders; `--tenant-primary` e cores configuráveis podem gerar contraste insuficiente | `Views/render/buttons.blade.php:11` |
| Mensagens de erro | `alert` nativo, sem associação ao campo | `sections-manager.blade.php:319-331` |
| Alto contraste | classes `.acessibilidade-alto-contraste` aplicadas a fundos/textos do viewer | `landing-sections-viewer.blade.php:16-31` |

## Rotas e endpoints

O componente consome as rotas nomeadas do serviço (nenhuma rota própria):

| Uso no JS | Rota | Método | Payload | Resposta usada |
|---|---|---|---|---|
| `urls.getForm` | `v2.landing-sections.form` | GET | `type`, `section_id`, `parameters` (JSON string) | HTML injetado no body do modal |
| `urls.validate` | `v2.landing-sections.validate` | POST | campos serializados + `type` + `section_id` | `success` ou `errors` (422) |
| `urls.savePreview` | `v2.landing-sections.preview.save` | POST | `token`, `sections` (JSON string) | `success` → `refreshPreview()` |
| `urls.renderPreview` | `v2.landing-sections.preview.render` | GET | `token` na query | página carregada no iframe |
| `urls.renderSection` | `v2.landing-sections.render-section` | POST | `type`, `parameters` (JSON string) | `html` para `postMessage` |
| `urls.mediaList`/`urls.mediaStore` | `v2.landing-sections.media.*` | GET/POST | — | definidas no IIFE, sem uso no fluxo vivo |

Fontes: `V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:120-128`; `V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:200-220,303-317`; `V2/Services/LandingSectionsEditor/Views/components/forms/logic/preview-manager.blade.php:17-41`.

## Dados e migrations

A UI não cria tabelas. O JSON do hidden é persistido pelo consumidor em `v2_layout_base_configs.payload` (jsonb, cast array) e o preview em cache `landing_preview_<token>` (3600 s). Imagens referenciadas são IDs de `v2_gallery_media`. Fontes: `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:13-27`; `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:153`.

## 6. Catálogo de arquivos

| Arquivo | Papel |
|---|---|
| `Views/components/forms/landing-sections-editor.blade.php` | Componente principal: props, hidden, sidebar, iframe, modal, gallery, script |
| `Views/components/forms/image-picker.blade.php` | Campo de imagem (hidden + prévia + botões) |
| `Views/components/forms/logic/state-loader.blade.php` | Parse do valor inicial |
| `Views/components/forms/logic/preview-manager.blade.php` | `persistState`, `triggerPreviewSave`, `refreshPreview` |
| `Views/components/forms/logic/media-manager.blade.php` | `selectImage` |
| `Views/components/forms/logic/sections-manager.blade.php` | Modal, eventos, `loadForm`, `saveSection`, `removeSection`, `renderSidebar`, `generateUuid`, `init` |
| `Views/components/forms/logic/section-modal.blade.php` | Markup do overlay + painel |
| `Views/components/forms/logic/styles.blade.php` | CSS do editor/modal (`@push('css')`) |
| `Views/components/forms/logic/image-picker-modal.blade.php` | Picker local legado (não incluído; morto) |
| `Views/components/landing-sections-viewer.blade.php` | Viewer público + estilos de alto contraste |
| `Views/preview.blade.php` | Página do iframe de preview + listener `postMessage` |

## 7. Configuração e permissões

Não há config própria. O comportamento depende de `name` (persistência), do catálogo do registry (`getSectionTypes`), das rotas nomeadas `v2.landing-sections.*` e da presença do `gallery-picker`. O editor não checa permissão; o botão de salvar do consumidor pode ser protegido por `@v2can` (ex.: `V2/Modules/Institucional/SobreNos/Views/cms/editor.blade.php:9-14`).

## 8. Observabilidade e erros

| Situação | Sinal | Fonte |
|---|---|---|
| Valor inicial inválido | `console.error('[LandingSectionsEditor] Failed to parse initial value:', e)` | `state-loader.blade.php:7` |
| Form em cache | `console.log('[LandingSectionsEditor] Form obtido do cache:', cacheKey)` | `sections-manager.blade.php:195` |
| Falha ao carregar form | HTML `Erro ao carregar o formulario da secao.` | `:208` |
| 422 | `alert` com `- <campo>: <mensagens>` | `:319-327` |
| Outro erro de validação | `alert('Erro na validacao dos dados da secao.')` | `:329-331` |
| Upload no picker morto | `toastr.error('Erro ao fazer upload da imagem.')` ou `alert('Erro ao fazer upload da imagem.')` | `image-picker-modal.blade.php:172-177` |

Não há telemetria de front-end; erros de rede em `/preview/save` são silenciosos (`triggerPreviewSave` só trata `success`, `preview-manager.blade.php:25-29`).

## 9. Consumidores e dependências

| Relação | Ator | Uso | Risco |
|---|---|---|---|
| Consumidor | `MOD-LAYOUTS-HOMEPAGE` | Editor em `Views/cms/editor.blade.php:51-55`; viewer em `Views/portal/index.blade.php:34-36` | Alto se mudarem IDs/props ou o nome `landing_sections_json` |
| Consumidor | `MOD-INSTITUCIONAL-SOBRENOS` | Editor em `Views/cms/editor.blade.php:22-26`; viewer em `Views/portal/index.blade.php:25-27` | Alto; forms do módulo incluem `image-picker` do LSE |
| Dependência (LSE → Gallery) | `SVC-GALLERY` | `gallery-picker` com `name=lse_global_media_picker`, `ownerModule=landing_page` | Médio: mudança no evento `lotep:open-gallery-picker-*` quebra a seleção |
| Dependência (LSE → CRE) | `SVC-CONTENTRICHEDITOR` | `assets_only` + `content-rich-editor` nos forms | Médio |

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| LG-LSE-12 | Não há teste de UI/DOM do editor (somente o CSS do modal). | Regressões de IDs/eventos passam despercebidas. | `tests/Unit/V2/Services/LandingSectionsEditor/LseModalStylesTest.php` |
| LG-LSE-13 | Não se sabe se `image-picker-modal` é usado fora do V2. | Decisão de remoção. | `Views/components/forms/logic/image-picker-modal.blade.php` |
| LG-LSE-14 | `selectImage` aparece em `media-manager` e em `image-picker-modal`; o segundo está fora de escopo e usa `window.lotepGlobalMediaPickerInitialized`. | Duplicação de comportamento. | `media-manager.blade.php:4`; `image-picker-modal.blade.php:71-72` |
| LG-LSE-15 | Não há medição de performance do `renderSidebar` em páginas longas. | Risco de UX com muitas seções. | `sections-manager.blade.php:356-388` |

## 11. Checklist de paridade

- [ ] `PC-LSE-25` — O hidden tem `id="landing-sections-hidden-<name>"` e `name="<name>"`. Fonte: `landing-sections-editor.blade.php:24`.
- [ ] `PC-LSE-26` — O painel do modal tem `role="dialog"`, `aria-modal="true"`, `aria-labelledby` e header com “Configurar Seção”. Fonte: `section-modal.blade.php:4-6`.
- [ ] `PC-LSE-27` — O modal não contém `.modal-dialog`; `modal-xl` não se aplica. Fonte: `section-modal.blade.php:1-17`.
- [ ] `PC-LSE-28` — Clique em “ADICIONAR SEÇÃO” dispara `GET /form` com `type`, `section_id` e `parameters` JSON. Fonte: `sections-manager.blade.php:52-55,200-206`.
- [ ] `PC-LSE-29` — Salvar válido atualiza `sections`, hidden, sidebar e preview, e emite `lotep:update-section-html`. Fonte: `:280-317`.
- [ ] `PC-LSE-30` — 422 exibe `alert` com uma linha por campo. Fonte: `:319-327`.
- [ ] `PC-LSE-31` — Selecionar mídia dispara `lotep:open-gallery-picker-lse_global_media_picker` com `callback` e atualiza input/prévia/thumbnail. Fonte: `:121-131`; `media-manager.blade.php:4-14`.
- [ ] `PC-LSE-32` — Preview escuta `lotep:update-section-html` e substitui o bloco; ausente → `location.reload()`. Fonte: `preview.blade.php:22-34`.
- [ ] `PC-LSE-33` — `persistState` faz debounce de 800 ms antes de `POST /preview/save`. Fonte: `preview-manager.blade.php:10-11`.
- [ ] `PC-LSE-34` — O viewer gera `div.lse-render-section-block[data-section-id]` e omite tipos sem view. Fonte: `landing-sections-viewer.blade.php:40-47`.
- [ ] `PC-LSE-35` — CSS do modal mantém `flex-direction: column`, body com `overflow-y:auto` e `min-height:0`. Fonte: `styles.blade.php:122-175`; teste `LseModalStylesTest.php:18-46`.

## 12. Evidências consultadas

- `V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:1-158`
- `V2/Services/LandingSectionsEditor/Views/components/forms/logic/{state-loader,preview-manager,media-manager,sections-manager,section-modal,styles,image-picker-modal}.blade.php`
- `V2/Services/LandingSectionsEditor/Views/components/forms/image-picker.blade.php:1-29`
- `V2/Services/LandingSectionsEditor/Views/components/landing-sections-viewer.blade.php:1-49`
- `V2/Services/LandingSectionsEditor/Views/preview.blade.php:1-37`
- `V2/Services/LandingSectionsEditor/Views/forms/hero.blade.php:1-58`
- `V2/Services/Gallery/Views/components/logic/scripts/main-orchestrator.blade.php:1-80`
- `V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:46-56`; `V2/Modules/Layouts/Homepage/Views/portal/index.blade.php:33-36`
- `V2/Modules/Institucional/SobreNos/Views/cms/editor.blade.php:17-27`; `V2/Modules/Institucional/SobreNos/Views/portal/index.blade.php:24-27`
- `tests/Unit/V2/Services/LandingSectionsEditor/LseModalStylesTest.php:18-59`
