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

# Widgets e schemas do LandingSectionsEditor

Este documento é o catálogo dos tipos de seção e do contrato para criar novos widgets. Cobre
`Widgets/`, `ValidationSchemas/`, `Services/WidgetRegistry.php`, `Views/forms/` e `Views/render/`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Define 22 tipos de seção (cada um com schema, form e render) e o protocolo para que módulos registrem tipos próprios com dados dinâmicos. |
| Entrada | `WidgetRegistry::register(slug, widget)` no boot do módulo; `type` nas rotas `/form`, `/validate` e `/render-section`; `parameters` no payload. |
| Saída | Catálogo do dropdown; HTML de formulário; regras de validação; HTML de seção. |
| Efeitos colaterais | Registro estático global por requisição; nenhum cache de registry; cache de formulários no cliente. |
| Particularidade central | Só 21 dos 22 tipos estão registrados; `video` tem schema/form/render mas não aparece no editor. Módulos podem substituir tipos padrão. |
| Como se integra | `class MeuWidget extends BaseWidget` com `$schemaClass`, `$formView`, `$renderView`; registrar no construtor do `ModuleProvider`. |
| Se removido, quebra | Todos os renders de portal das páginas com seções e os editores de Homepage/Sobre Nós. |
| Superfície pública | `WidgetContract` (8 métodos), `BaseWidget`, `GenericWidget`, `WidgetRegistry` (4 métodos), `ValidationService`, 22 classes `*Schema`. |

## 1. Efeito

### 1.1 Transformação que o catálogo realiza

`type` → widget → (schema, form, render). No editor, o `type` vindo de `data-type` define qual HTML é carregado (`Views/components/forms/logic/sections-manager.blade.php:52-55`). Na validação, o `type` define as regras (`ValidationSchemas/ValidationService.php:37-52`). No portal, o `type` define a view (`Views/components/landing-sections-viewer.blade.php:36-46`).

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Popula o registry com 21 padrões | memória do processo | `registerLegacyWidgets` | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:36-74` |
| Resolve regras de validação por registry ou schema | memória | `getRulesFor` | `V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php:37-52` |
| Injeta caminho/variantes de mídia nos parâmetros | array de seções | `prepare` + `array_set` | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:185-197` |
| Renderiza HTML por tipo | resposta HTTP/portal | views `render/*` | `V2/Services/LandingSectionsEditor/Views/render/` |

### 1.3 Contrato observável

- O catálogo exposto ao CMS é `['icon' => string, 'title' => string]` por slug (`LandingSectionsEditorRendererService.php:92-105`).
- `getValidationRules()` devolve `[]` quando o widget não tem `schemaClass` ou a classe não implementa `rules()` (`Widgets/BaseWidget.php:43-52`).
- `getFormView()`/`getRenderView()` devolvem o valor configurado ou inferem `v2-service-landingsectionseditor::forms|render.<slug>` a partir do nome da classe (`Widgets/BaseWidget.php:72-93,154-159`).
- `getAttachmentIds()` devolve `array<caminho, id>`; `prepare()` devolve os parâmetros enriquecidos (`Widgets/WidgetContract.php:50-59`).

### 1.4 O que o catálogo NÃO faz

- Não descobre widgets por convenção de diretório: é necessário registrar.
- Não valida a existência das views no registro; o erro só aparece em `getForm`/`renderSection`.
- Não impede colisão de slug entre módulos.
- Não registra `video` (schema/views existem, mas o slug não está em `$legacyConfigs`).

## 2. Abordagem

### 2.1 Estratégia e por quê

`WidgetContract` é a interface mínima (`Widgets/WidgetContract.php:12-59`). `BaseWidget` implementa defaults e permite que subclasses sobrescrevam apenas o necessário; `GenericWidget` é um value object para tipos sem lógica (`Widgets/GenericWidget.php:11-27`). O schema é uma classe estática com `rules()` — não é um `FormRequest` — para poder ser resolvido por convenção de nome em `ValidationService` (`:44-49`). O par de views (`forms.<type>`, `render.<type>`) é resolvido por nome, o que permite fallback sem widget.

### 2.2 Mecânica passo a passo

1. Boot do módulo: `new ModuleProvider()` → `WidgetRegistry::register($slug, new Widget())` (`V2/Modules/Layouts/Homepage/ModuleProvider.php:20-32`; `V2/Modules/Institucional/SobreNos/ModuleProvider.php:18-25`).
2. Primeiro `getWidgets()` → `registerLegacyWidgets()` preenche os slugs ausentes com `GenericWidget` e schema padrão (`LandingSectionsEditorRendererService.php:81-90`, `:62-73`).
3. Editor: `getSectionTypes()` → dropdown particionado em “Padrão” e “Personalizados” por prefixo `sucesu-`/`sobre-nos-` (`Views/components/forms/landing-sections-editor.blade.php:35-70`).
4. Form: `/form?type=<slug>` → `$widget->getFormView()` ou `forms.<slug>` (`Controllers/LandingSectionsEditorController.php:85-93`).
5. Validação: `/validate` → `getRulesFor($type)` → `$widget->getValidationRules()` ou `<StudlyType>Schema::rules()` (`ValidationSchemas/ValidationService.php:16-52`).
6. Persistência: o JSON montado pelo cliente (`parameters[...]`) é gravado pelo consumidor em `v2_layout_base_configs.payload`.
7. Render: `prepareSections()` → `prepare()` → view `$widget->getRenderView()` (`LandingSectionsEditorRendererService.php:128-205`; `Views/components/landing-sections-viewer.blade.php:33-48`).

### 2.3 Trechos de código pivô

```php
// V2/Services/LandingSectionsEditor/Widgets/WidgetContract.php:42-59
// Os três métodos que ligam o widget às views e à resolução de mídia.
public function getRenderView(): string;

/**
 * Coleta IDs de anexos (GalleryMedia) que este widget precisa resolver.
 *
 * @param array $parameters
 * @return array
 */
public function getAttachmentIds(array $parameters): array;

/**
 * Enriquece parâmetros com dados de banco (URLs de imagens, etc.)
 */
public function prepare(array $parameters, Collection $anexos = null): array;
```

**Efeito.** Um widget só precisa declarar como renderizar e como resolver mídia; o schema é opcional e as views têm fallback.

```php
// V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:43-52
// Regras vêm da classe de schema, instanciada por requisição.
public function getValidationRules(): array
{
    if ($this->schemaClass && class_exists($this->schemaClass)) {
        $schema = new $this->schemaClass();
        if (method_exists($schema, 'rules')) {
            return $schema->rules();
        }
    }
    return [];
}
```

**Efeito.** `rules()` é chamado em instância (`new $this->schemaClass()`), embora todos os schemas padrão declarem `static`. Funciona porque PHP permite chamar método estático em instância.

```php
// V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php:37-52
// Registry primeiro; fallback por convenção de nome no namespace do LSE.
$widget = WidgetRegistry::get($type);
if ($widget) {
    return $widget->getValidationRules();
}

$className = Str::studly($type) . 'Schema';
$schemaClass = 'V2\Services\LandingSectionsEditor\ValidationSchemas\\' . $className;

if (class_exists($schemaClass) && method_exists($schemaClass, 'rules')) {
    return $schemaClass::rules();
}

return [];
```

**Efeito.** Slugs com hífen funcionam (`cards-numbered` → `CardsNumberedSchema`). Slugs de módulo (`sucesu-hero`) não têm schema no namespace do LSE: sem registro, a validação fica vazia.

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuHeroWidget.php:29-38
// Módulo declara o caminho aninhado de cada mídia que quer receber.
public function getAttachmentIds(array $parameters): array
{
    $ids = [];
    foreach ($parameters['slides'] ?? [] as $i => $slide) {
        if (!empty($slide['image_id'])) {
            $ids["slides.{$i}.image"] = $slide['image_id'];
        }
    }
    return $ids;
}
```

**Efeito.** `prepareSections` injeta `slides[<i>][image]` e, quando existirem, `slides[<i>][image_variants]`. O caminho é também a chave usada para `*_variants`.

```php
// V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:106-121
// Implementação default: copia file_path para o caminho declarado.
public function prepare(array $parameters, Collection $anexos = null): array
{
    if (!$anexos || $anexos->isEmpty()) {
        return $parameters;
    }

    $ids = $this->getAttachmentIds($parameters);
    if (empty($ids)) {
        return $parameters;
    }

    foreach ($ids as $fieldPath => $id) {
        if ($m = $anexos->get($id)) {
            $parameters = $this->setNestedValue($parameters, $fieldPath, $m->file_path);
        }
    }

    return $parameters;
}
```

**Efeito.** O valor injetado é `file_path` cru, **não** URL. Quem monta URL é a view do módulo (`StorageService::url`/`v2-service-media::components.picture`).

```php
// V2/Modules/Institucional/SobreNos/LseWidgets/Widgets/SobreNosTestimonialWidget.php:26-42
// Mapeamento não aninhado: parâmetro -> chave de saída.
public function getAttachmentIds(array $parameters): array
{
    $ids = [];
    $fields = [
        'image_id'        => 'image',
        'client_image_id' => 'client_image',
        'icon_id'         => 'icon',
        'shape_1_id'      => 'shape_1',
        'shape_2_id'      => 'shape_2',
    ];
    foreach ($fields as $paramKey => $fieldPath) {
        if (!empty($parameters[$paramKey])) {
            $ids[$fieldPath] = $parameters[$paramKey];
        }
    }
    return $ids;
}
```

**Efeito.** Slugs de módulo podem usar mapeamentos arbitrários (sem caminho com pontos); os renders recebem `image`, `client_image`, `icon` etc. e as variantes como `image_variants` etc.

```php
// V2/Services/LandingSectionsEditor/Widgets/GenericWidget.php:11-27
// Tipos padrão são apenas configuração; não têm comportamento próprio.
public function __construct(
    string $icon,
    string $title,
    string $schemaClass = null,
    string $formView = null,
    string $renderView = null
) {
    $this->icon = $icon;
    $this->title = $title;
    $this->schemaClass = $schemaClass;
    $this->formView = $formView;
    $this->renderView = $renderView;
}
```

**Efeito.** `BaseWidget::sanitize()` (strip_tags) e `prepare()` (sem attachments) valem para todos os tipos padrão. É a causa de as imagens padrão não receberem `*_url` (ver §4.3).

### 2.4 Algoritmos e custos

- Registro: O(21) por primeiro `getWidgets()`; cada chamada posterior repete o loop com 21 `has()` (barato, mas sem memoização).
- Resolução de schema: O(1) no registry; fallback faz `class_exists` (autoload).
- `getAttachmentIds` por widget é O(itens do array); `prepareSections` consolida todos e faz uma consulta (O(S+M)).
- Nenhum schema usa `Rule::unique` ou consultas; apenas `exists:v2_gallery_media,id` (por exemplo `CardsNumberedSchema.php:16`), que executa uma consulta por campo validado.

### 2.5 Contrato de ponta a ponta (`sucesu-hero`)

Fluxo completo, com os artefatos reais:

1. **Registro.** `WidgetRegistry::register('sucesu-hero', new SucesuHeroWidget())` no construtor do provider (`V2/Modules/Layouts/Homepage/ModuleProvider.php:22`). O widget aponta para schema, form e render (`V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuHeroWidget.php:14-21`).
2. **Schema.** `SucesuHeroSchema::rules()` valida `parameters.slides.*` (`V2/Modules/Layouts/Homepage/LseWidgets/Schemas/SucesuHeroSchema.php:7-17`).
3. **Form.** `GET /cms/v2/landing-sections/form?type=sucesu-hero&section_id=&parameters={}` devolve `v2-homepage::lse.forms.hero`. Cada slide existente vem de `_hero-slide-card` com `value` preenchido (`V2/Modules/Layouts/Homepage/Views/lse/forms/_hero-slide-card.blade.php:16-53`); novos slides usam `<template>` e são reindexados via JS (`V2/Modules/Layouts/Homepage/Views/lse/forms/hero.blade.php:32-82,109-155`).
4. **Validação.** `POST /cms/v2/landing-sections/validate` recebe o form **urlencoded** (`parameters[slides][0][title]=...&type=sucesu-hero`), não JSON. `ValidationService::validate()` aplica `Validator::make($request->all(), $rules)` (`V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php:16-25`), e o widget fornece as regras (`Widgets/BaseWidget.php:43-52`).
5. **Persistência.** Em 200, o JS monta o objeto aninhado e insere a seção em `sections` (`sections-manager.blade.php:223-294`). O submit externo envia `landing_sections_json`; `HomepageCmsController::saveSections` grava em `v2_layout_base_configs` com `key=landing_sections` (`V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:59-70`).
6. **Render.** `HomepagePortalController` chama `HomepageDataIntegrator::integrate` e `prepareSections` (`V2/Modules/Layouts/Homepage/Controllers/HomepagePortalController.php:44-48`); o widget resolve `slides.{i}.image`/`image_variants` (`SucesuHeroWidget.php:29-58`) e a view `v2-homepage::lse.render.hero` monta `picture` responsivo com `StorageService` (`V2/Modules/Layouts/Homepage/Views/lse/render/hero.blade.php:1-37`).

Payload persistido resultante:

```json
[
  {
    "id": "b1b7c1e2-3a4d-4f5e-8a90-1c2d3e4f5a6b",
    "type": "sucesu-hero",
    "order": 1,
    "parameters": {
      "slides": [
        {
          "title": "SUCESU — Tecnologia que transforma",
          "subtitle": "Conectando profissionais de TI em todo o Brasil",
          "cta_text": "Saiba mais",
          "cta_url": "/sobre",
          "image_id": "42",
          "image": "sucesu/hero/heroThumb1_1.jpg",
          "image_variants": { "480": "sucesu/hero/480/heroThumb1_1.webp" }
        }
      ]
    }
  }
]
```

Observação: `image`, `image_variants` e `image_url` **não** são gravados pelo editor; são injetados em tempo de render por `prepare()`/`prepareSections()` e existem apenas no array em memória. O payload salvo contém somente os IDs (`image_id`). `order` é posicional (1..n) e não é usado para ordenar o render.

## 3. Particularidades

### 3.1 O que é exclusivo deste catálogo

O único contrato do sistema que combina **schema estático + par de views + resolução de mídia por caminho aninhado**. Nenhum outro serviço define tipos de seção; `StructuralLandingEditor` tem blocos com estrutura própria, e o Gallery só armazena mídia.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-LSE-09 | O slug é a identidade; schema/form/render/validação dependem dele. | Renomear slug quebra payloads e views. | `Controllers/LandingSectionsEditorController.php:86-91` |
| RN-LSE-10 | `getAttachmentIds()` retorna `caminho => id` (não lista). | Um retorno indexado numericamente grava variantes em chaves `0_variants` etc. | `Widgets/WidgetContract.php:50-52`; `LandingSectionsEditorRendererService.php:189-196` |
| RN-LSE-11 | `prepare()` deve devolver os parâmetros completos, não só os enriquecidos. | Devolver apenas `image` apaga título/texto. | `Widgets/BaseWidget.php:106-130` |
| RN-LSE-12 | Schema tem `rules()` estático, mas é chamado em instância. | Declarar `rules()` não estático também funciona; declarar construtor com dependências quebra. | `Widgets/BaseWidget.php:45-48` |
| RN-LSE-13 | Só 21 slugs padrão são registrados; `video` não. | `video` não aparece no dropdown nem em `getSectionTypes`. | `LandingSectionsEditorRendererService.php:38-60` |
| RN-LSE-14 | Precedência: módulo > padrão; entre módulos, último registra vence. | Colisão silenciosa. | `LandingSectionsEditorRendererService.php:62-73`; `WidgetRegistry.php:19-21` |
| RN-LSE-15 | `sanitize()` de `BaseWidget` remove todas as tags de strings. | Preview de rich text e `html-free` perde formatação. | `Widgets/BaseWidget.php:57-65` |

### 3.3 Casos-limite

| Caso | Comportamento | Fonte |
|---|---|---|
| Schema ausente | `getValidationRules()` → `[]`; `/validate` responde 200. | `Widgets/BaseWidget.php:43-52`; `ValidationService.php:20-25` |
| Schema com `rules()` privado | `method_exists` é verdadeiro; chamada falha com erro fatal. | `Widgets/BaseWidget.php:46-49` |
| `schemaClass` inexistente | `class_exists` falso → `[]`. | `Widgets/BaseWidget.php:45` |
| View de form ausente | `/form` → 404 com o slug. | `Controllers/LandingSectionsEditorController.php:89-91` |
| View de render ausente | `/render-section` → 404; no portal a seção é omitida. | `:226-230`; `Views/components/landing-sections-viewer.blade.php:40-47` |
| Tipo sem `title`/`icon` | Dropdown mostra vazio; a sidebar usa fallback `bi bi-box` + slug. | `Views/components/forms/landing-sections-editor.blade.php:53-58`; `sections-manager.blade.php:366` |
| Colisão de slug entre módulos | Um dos dois widgets é descartado silenciosamente. | `WidgetRegistry.php:19-21` |
| Mídia referenciada mas removida | `get()` nulo → nenhum caminho/variante injetado. | `LandingSectionsEditorRendererService.php:190-196` |
| `parameters` não-array | `prepare()` recebe `[]` e devolve `[]`. | `LandingSectionsEditorRendererService.php:136` |

### 3.4 Armadilhas de reimplementação

1. **Retornar lista em `getAttachmentIds()`** em vez de mapa: `array_set($params, "0_variants", ...)` cria chave inválida e nenhuma variante chega à view.
2. **Usar `$this->renderView` sem namespace completo**: `view()->exists()` falha e o tipo é omitido no portal.
3. **Devorar `parameters` em `prepare()`** (retornar apenas dados resolvidos) apaga os campos editáveis do payload. `prepare` deve enriquecer, não substituir.
4. **Contar com `video`**: não há widget; formas de incluí-lo no editor exigem registro explícito. O render tem ainda um bug de `id` (`Views/render/video.blade.php:10`).
5. **Depender de `rules()` no namespace LSE para slug de módulo**: `Str::studly('sucesu-hero')` gera `SucesuHeroSchema` em `V2\Services\LandingSectionsEditor\ValidationSchemas`, que não existe. O schema do módulo só é usado via widget.

### 3.5 Dívidas e divergências

| Item | Evidência | Impacto |
|---|---|---|
| `video` é o 22º tipo, mas não está registrado. | `ValidationSchemas/VideoSchema.php:4-13`; `LandingSectionsEditorRendererService.php:38-60` | Tipo inacessível pelo editor; `Views/forms/video.blade.php` e `Views/render/video.blade.php` ficam órfãos. |
| `TextOnlySchema` aceita `text_color` hex, mas o render só reconhece `light`; qualquer outro valor vira `text-dark`. | `ValidationSchemas/TextOnlySchema.php:14`; `Views/render/text-only.blade.php:6-9` | Cor de texto do formulário (color picker) não tem efeito. |
| `video-playlist` promete título do vídeo via `oEmbed`, mas o seletor `.video-title-placeholder` não existe no HTML. | `Views/render/video-playlist.blade.php:62,87-100` | Rótulos permanecem “Carregando...”. |
| `interactive-table` usa TSV (`fgetcsv` com `"\t"`), não HTML. | `Views/render/interactive-table.blade.php:21-30` | Reimplementar como HTML quebra o formato do campo. |
| Prefixos “Personalizados” são hardcoded. | `Views/components/forms/landing-sections-editor.blade.php:36` | Novo módulo com outro prefixo cai em “Padrão”. |
| `README.md` afirma 22 tipos. | `V2/Services/LandingSectionsEditor/README.md:24-27` | Divergência com o registry (21). |

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Consumidores |
|---|---|---|---|
| `WidgetContract::getIcon` | `(): string` | classe de ícone Bootstrap (`bi bi-*`) | dropdown |
| `WidgetContract::getTitle` | `(): string` | rótulo humano | dropdown/sidebar |
| `WidgetContract::getValidationRules` | `(): array` | regras Laravel | `ValidationService` |
| `WidgetContract::sanitize` | `(array $parameters): array` | parâmetros sem tags | `sanitizeContent` |
| `WidgetContract::getFormView` | `(): string` | nome de view | `getForm` |
| `WidgetContract::getRenderView` | `(): string` | nome de view | `renderSection`, viewer |
| `WidgetContract::getAttachmentIds` | `(array $parameters): array` | `caminho => id` | `prepareSections` |
| `WidgetContract::prepare` | `(array $parameters, ?Collection $anexos): array` | parâmetros enriquecidos | `prepareSections` |
| `BaseWidget::setNestedValue` | `(array $array, string $path, mixed $value): array` | array com valor no caminho | `prepare` |
| `BaseWidget::inferType` | `(): string` | `SucesuHeroWidget` → `sucesu-hero` | fallback de views |

### 4.2 Componentes Blade e contrato de UI

Cada tipo tem um form em `Views/forms/<slug>.blade.php` e um render em `Views/render/<slug>.blade.php`. Form recebe `$data` (array de parâmetros), `$sectionId`, `$type` (`Controllers/LandingSectionsEditorController.php:93`); render recebe `$section` e `$data = $section['parameters']` (`Views/components/landing-sections-viewer.blade.php:42-45`). Os forms usam Collective `Form::*` e names `parameters[...]`; o JS do editor depende desse prefixo para montar o objeto (`sections-manager.blade.php:227-271`).

### 4.3 JS e ciclo de vida

O catálogo não tem JS próprio; os forms podem incluir scripts inline (ex.: `Views/forms/hero.blade.php:53-58` usa `ColorManager`). O carregamento do form reinicializa componentes via `UIManager.initSelect2`, `UIManager.initIconPickers` e `window.initContentRichEditors` (`sections-manager.blade.php:184-191`). Sem `UIManager`, selects/icon pickers ficam sem comportamento, mas o HTML funciona.

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

1. O módulo deve existir como `V2/Modules/<Grupo>/<Modulo>` com `ModuleProvider`.
2. O `ModuleProvider` deve implementar `ModuleProviderInterface` e registrar no **construtor** (`V2/Modules/Layouts/Homepage/ModuleProvider.php:20-32`), porque o provider é instanciado no boot (`V2/Core/Providers/V2ServiceProvider.php:104-105`) e o primeiro render pode ocorrer depois.
3. Widgets padrão entram depois, no primeiro `getWidgets()`.
4. Views do módulo precisam estar no namespace `v2-<modulo-minusculo>` (ex.: `v2-homepage::lse.forms.hero`, `v2-sobrenos::lse.render.team`), carregado pelo provider.
5. O schema do módulo pode ficar em qualquer namespace, desde que a classe seja referenciada em `$schemaClass`.

### 4.5 Receitas de uso

**1. Registrar um widget completo (módulo).**

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuHeroWidget.php:14-21
public function __construct()
{
    $this->icon = 'bi bi-star-fill';
    $this->title = 'Hero SUCESU';
    $this->schemaClass = SucesuHeroSchema::class;
    $this->formView = 'v2-homepage::lse.forms.hero';
    $this->renderView = 'v2-homepage::lse.render.hero';
}
```

Resultado: `sucesu-hero` no dropdown “Personalizados”, form/render do módulo, validação por `SucesuHeroSchema`.

**2. Declarar mídia resolvida.**

```php
// V2/Modules/Institucional/SobreNos/LseWidgets/Widgets/SobreNosTestimonialWidget.php:26-42
public function getAttachmentIds(array $parameters): array
{
    $ids = [];
    $fields = [
        'image_id'        => 'image',
        'client_image_id' => 'client_image',
        'icon_id'         => 'icon',
        'shape_1_id'      => 'shape_1',
        'shape_2_id'      => 'shape_2',
    ];
    foreach ($fields as $paramKey => $fieldPath) {
        if (!empty($parameters[$paramKey])) {
            $ids[$fieldPath] = $parameters[$paramKey];
        }
    }
    return $ids;
}
```

Resultado: o render recebe `$data['image']` (file_path), `$data['image_variants']` e equivalentes.

**3. Criar um tipo padrão novo (sem módulo).**

```php
// V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:38-41
$legacyConfigs = [
    'hero' => ['bi bi-star-fill', 'Hero (Destaque Principal)', HeroSchema::class],
    // adicionar: 'meu-tipo' => ['bi bi-box', 'Meu Tipo', MeuTipoSchema::class],
];
```

Resultado: o tipo entra no dropdown “Padrão” e resolve `forms.meu-tipo`/`render.meu-tipo` no namespace do serviço. Requer criar `ValidationSchemas/MeuTipoSchema.php`, `Views/forms/meu-tipo.blade.php` e `Views/render/meu-tipo.blade.php`.

**4. Validar diretamente pelo schema (sem widget).**

```php
$ok = \Validator::make($request->all(), (new \V2\Services\LandingSectionsEditor\ValidationSchemas\CarouselSchema())->rules())->passes();
```

Resultado: mesmas regras usadas por `/validate` quando não há widget registrado.

### 4.6 Anti-uso

1. **Criar widget e registrar só no `routes` ou na view**: `WidgetRegistry` é populado apenas por `register`; sem isso, o slug não aparece e o form cai no namespace do serviço (provavelmente inexistente → 404).
2. **Devolver um array novo em `prepare()`**: o método substitui `$section['parameters']` (`LandingSectionsEditorRendererService.php:186`); retornar só `['members' => ...]` apaga título/subtítulo configurados e re-salvar no editor perde os campos.
3. **Usar `parameters.text_color` como hex em `text-only`**: o render compara com `'light'` (`Views/render/text-only.blade.php:9`), então qualquer hex resulta em `text-dark`.

### 4.7 Custos e limites

- Regras `exists:v2_gallery_media,id` executam consulta por item (ex.: 10 cards = 10 consultas) no `/validate`.
- `interactive-table` não limita linhas/colunas; o conteúdo TSV inteiro é processado em memória com `fgetcsv` (`Views/render/interactive-table.blade.php:23-30`).
- `html-free` executa HTML arbitrário em iframe `sandbox="allow-scripts allow-forms allow-popups allow-modals"` (`Views/render/html-free.blade.php:66`) — isolado por origem, mas com scripts permitidos.
- O cache no cliente de forms não tem limite nem versão por schema (`sections-manager.blade.php:3,178`).

## 5. Rotas e endpoints

O catálogo não declara rotas; consome as rotas do serviço (`V2/Services/LandingSectionsEditor/Routes/cms.php:12-16`) com `type` como discriminador. Para um tipo novo, nenhuma rota precisa ser criada: `/form`, `/validate` e `/preview/render-section` passam a atendê-lo automaticamente.

## 6. Dados e migrations

Nenhuma tabela própria. Persistência do payload em `v2_layout_base_configs.payload` (jsonb) e mídias em `v2_gallery_media` (`V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php:22-29`; `LandingSectionsEditorRendererService.php:171-174`). Formatos de `parameters` por tipo estão na tabela §7.

## Configuração e permissões

O catálogo não lê config/env. O comportamento depende de:

| Item | Efeito | Fonte |
|---|---|---|
| `$legacyConfigs` (21 slugs) | Define quais tipos padrão existem e em que ordem aparecem em “Padrão”. | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:38-60` |
| Prefixos `sucesu-`/`sobre-nos-` | Classificam tipos como “Personalizados” no dropdown. | `V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:36` |
| `$schemaClass`/`$formView`/`$renderView` | Contrato por widget; sem eles valem os fallbacks por nome de classe/slug. | `V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:43-93` |
| Permissões | Nenhuma no catálogo nem nas rotas do serviço; só `CmsAuthenticate`. | `V2/Services/LandingSectionsEditor/Routes/cms.php:9` |

## Observabilidade e erros

| Situação | Sinal | Fonte |
|---|---|---|
| Schema/render ausente | 404 do controller com o slug na mensagem ou omissão silenciosa no portal. | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:89-91`; `Views/components/landing-sections-viewer.blade.php:40-47` |
| Schema sem `rules()` | Validação vazia (200). | `V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:43-52` |
| Mídia não resolvida | Nenhum log; view cai em placeholder/valor default. | `Views/render/only-image.blade.php:26-33`; `Views/render/grid.blade.php:30-36` |
| Falha na consulta de mídia | `[LandingSectionsEditor] Failed to fetch attachments: <msg>` e render sem imagens. | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:175-177` |
| Form em cache no cliente | `console.log('[LandingSectionsEditor] Form obtido do cache:', cacheKey)` | `V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:195` |

## 7. Catálogo dos 22 tipos

Valores em `parameters.*` conforme o schema; “render” descreve o efeito visual.

| # | type | schema | campos (`parameters.*`) | form | render/efeito visual | consumidores |
|---|---|---|---|---|---|---|
| 1 | `hero` | `HeroSchema` | `title` (req, ≤255), `subtitle` (≤255), `text_alignment` (`left|center|right`), `title_color`, `subtitle_color` (hex), `background_image_id` (exists) | `forms/hero.blade.php:1-51` | `<section class="hero-wrap">` full-width com `background_image_url`, overlay `rgba(0,0,0,.3)`, `h1` 48px e subtítulo uppercase, `min-height: 60vh`, alinhamento configurável (`render/hero.blade.php:10-35`) | dropdown Homepage/Sobre Nós; testes |
| 2 | `navigation` | `NavigationSchema` | `title` (≤255, default “Sumário da Página”), `background_color`, `remove_bottom_spacing` (bool), `items[].title` (req com items), `items[].target` (req com items, ≤255) | `forms/navigation.blade.php:5-60` | Caixa “sumário” com lista de âncoras, bolinha vermelha, linha pontilhada e “Ir para seção”; `id="summary-<section id>"` (`render/navigation.blade.php:8-45`) | dropdown |
| 3 | `icons-text` | `IconsTextSchema` | `background_color`, `text_color` (`dark|light`), `is_carousel` (bool), `items[].icon`, `items[].title` (req com items), `items[].text`, `items[].link` | `forms/icons-text.blade.php:6-99` | Grade de ícones 60px com título/texto; colunas 12/6/3 conforme 1/2/4 itens (senão 4); se `is_carousel`, vira Owl Carousel com setas (`render/icons-text.blade.php:9-84`) | dropdown |
| 4 | `text-image` | `TextImageSchema` | `title` (req), `subtitle`, `text`, `text_alignment` (`left|center|right|justify`), `image_id` (exists), `image_position` (`left|right|top|bottom`), `image_size_type` (`25|50|100|custom`), `image_size_custom` (1-100), `background_color`, `text_color` (hex), `button_text`, `button_link` | `forms/text-image.blade.php:3-110` | Texto + imagem com proporção calculada (`imageWidth` e `textWidth`); 100% força top/bottom; placeholder quando não há `image_url`; botão primário (`render/text-image.blade.php:19-71`) | dropdown |
| 5 | `text-only` | `TextOnlySchema` | `title`, `subtitle`, `text`, `text_alignment` (`...|justify`), `background_color`, `text_color` (hex), `background_image_id` (exists) | `forms/text-only.blade.php:3-53` | Bloco centralizado `col-md-10` com fundo (cor e/ou imagem cover); classe `text-dark`/`text-white` derivada de `text_color == 'light'` (`render/text-only.blade.php:2-36`) | dropdown |
| 6 | `only-image` | `OnlyImageSchema` | `title`, `subtitle`, `image_id` (**req**, exists), `background_color`, `image_size_type` (`25|50|100|custom`), `image_size_custom` (1-100) | `forms/only-image.blade.php:1-47` | Título/subtítulo + figura centralizada com largura em `%`; placeholder “Nenhuma imagem selecionada” (`render/only-image.blade.php:14-35`) | dropdown |
| 7 | `two-columns` | `TwoColumnsSchema` | `background_color`, `columns` (array `size:2`), `columns[].subtitle`, `columns[].title` (req com columns), `columns[].text_alignment`, `columns[].text` | `forms/two-columns.blade.php:4-39` | Duas colunas `col-md-6` (ou 12 se 1), cada uma com subtítulo, `h2` e rich text (`render/two-columns.blade.php:15-33`) | dropdown |
| 8 | `boxes` | `BoxesSchema` | `layout_type` (`vertical|horizontal`, default vertical), `background_color`, `items[].icon`, `items[].title` (req), `items[].text`, `items[].link` | `forms/boxes.blade.php:4-91` | Cartões `col-md-6`: vertical (ícone no topo, `h3`) ou horizontal (ícone à esquerda, `h4`); link recebe `/` quando relativo (`render/boxes.blade.php:17-53`) | dropdown |
| 9 | `carousel` | `CarouselSchema` | `title`, `subtitle`, `background_image_id` (exists), `items[].icon`, `items[].title` (req), `items[].text` | `forms/carousel.blade.php:3-85` | Seção com imagem de fundo fixa + overlay escuro; Owl Carousel 1/2/3 itens; cada item é cartão branco com ícone, título e rich text (`render/carousel.blade.php:15-76`) | dropdown |
| 10 | `cards-numbered` | `CardsNumberedSchema` | `title` (req), `subtitle`, `background_color`, `cards[].title` (req), `cards[].media_type` (`icon|image`, req), `cards[].icon` (req se icon), `cards[].image_id` (req se image, exists) | `forms/cards-numbered.blade.php:5-100` | Cabeçalho + cartões `col-md-4` com número grande no canto, ícone 50px ou imagem redonda 80×80 (`render/cards-numbered.blade.php:8-40`) | dropdown |
| 11 | `cards-links` | `CardsLinksSchema` | `background_color`, `cards[].title` (req), `cards[].icon`, `cards[].link` (req), `cards[].new_tab` (bool) | `forms/cards-links.blade.php:4-84` | Grade `col-md-3` de atalhos clicáveis, ícone grande, “Saiba mais”; `target` derivado de `new_tab` (`render/cards-links.blade.php:6-29`) | dropdown |
| 12 | `step-by-step` | `StepByStepSchema` | `title` (req), `subtitle`, `steps[].title` (req), `steps[].description`, `steps[].image_id` (exists) | `forms/step-by-step.blade.php:3-48` | Etapas `col-md-3`, imagem circular 180px (ou placeholder), badge numerado, seta entre etapas (`render/step-by-step.blade.php:14-52`) | dropdown |
| 13 | `video-playlist` | `VideoPlaylistSchema` | `title` (req), `subtitle`, `videos[].url` (req, `url`) | `forms/video-playlist.blade.php:3-29` | Player principal `youtube.com/embed/<id>` + lista lateral com thumbnails `mqdefault.jpg`; clique troca `src` (`render/video-playlist.blade.php:17-103`) | dropdown |
| 14 | `interactive-table` | `InteractiveTableSchema` | `title`, `subtitle`, `table_title`, `table_title_bg_color`, `table_title_text_color`, `table_title_alignment`, `table_content` (**req**), `has_header`, `striped`, `show_search` (bool), `search_position`, `text_alignment`, `table_width` (`100%|75%|50%|custom`), `table_width_custom` (1-100), `column_widths[]` | `forms/interactive-table.blade.php:5-435` | Tabela TSV com cabeçalho opcional, faixas, busca com debounce 200 ms (regex por termo), links auto-detectados, estado vazio (`render/interactive-table.blade.php:21-236`) | dropdown |
| 15 | `grid` | `GridSchema` | `title` (req), `subtitle`, `items[].title` (req), `items[].description`, `items[].image_id` (exists), `items[].button_title`, `items[].button_link` | `forms/grid.blade.php:3-63` | Grade `col-md-4` de cartões com imagem 200px (ou placeholder), título, rich text e botão (`render/grid.blade.php:14-54`) | dropdown |
| 16 | `questions-answers` | `QuestionsAnswersSchema` | `title` (req), `subtitle`, `background_color`, `items[].question` (req, ≤500), `items[].answer` (req), `items[].link_text`, `items[].link_url` (`url`) | `forms/questions-answers.blade.php:5-67` | Acordeão FAQ: cabeçalho clicável com ícone, resposta oculta, fecha os demais; link opcional “Acessar Link” (`render/questions-answers.blade.php:15-120`) | dropdown |
| 17 | `multi-card` | `MultiCardSchema` | `background_color`, `cards` (1-3), `cards[].title` (req), `cards[].title_color`, `cards[].subtitle`, `cards[].subtitle_color`, `cards[].content` (req), `cards[].text_color`, `cards[].background_color` | `forms/multi-card.blade.php:6-98` | 1 card full width, 2 em `col-md-6`, 3 em `col-md-4`; título 2.5rem, subtítulo uppercase, corpo rico dentro de cartão arredondado (`render/multi-card.blade.php:6-55`) | dropdown |
| 18 | `text-carousel` | `TextCarouselSchema` | `title` (req), `subtitle`, `text`, `button_text`, `button_link`, `items` (min 1), `items[].title` (req), `items[].symbol_type` (`icon|image`, req), `items[].icon` (req se icon), `items[].image_id` (req se image, exists) | `forms/text-carousel.blade.php:5-83` | Coluna esquerda com texto e botão; direita Owl Carousel de mini-cards; gradiente quando >3 itens; autoplay e loop condicional (`render/text-carousel.blade.php:17-122`) | dropdown |
| 19 | `command-line` | `CommandLineSchema` | `left_buttons[].text` (req), `.link`, `.icon`, `.style` (`limpo|padrao`, req), `right_buttons[...]` idem, `background_color` | `forms/command-line.blade.php:35-186` | Barra com botões à esquerda e à direita (tabela CSS); `limpo` transparente, `padrao` usa `--tenant-primary`; empilha no mobile (`render/command-line.blade.php:7-128`) | dropdown |
| 20 | `buttons` | `ButtonsSchema` | `buttons` (1-5), `buttons[].text` (req, ≤100), `.link` (req), `.bg_color` (req, hex), `.text_color` (req, hex) | `forms/buttons.blade.php:10-52` | Linha centralizada de botões grandes com cor de fundo/texto configuradas e sombra (`render/buttons.blade.php:5-18`) | dropdown |
| 21 | `html-free` | `HtmlFreeSchema` | `html_content` (**req**), `width_type` (`50|100|custom`), `width_custom` (1-100), `height_type` (`auto|double|custom`), `height_custom` (≥10) | `forms/html-free.blade.php:3-43` | Iframe isolado (`srcdoc`, `sandbox`), largura em `%`, altura 800px (`double`), `custom` px ou automática via `postMessage resize-iframe` (sem listener no preview) (`render/html-free.blade.php:1-72`) | dropdown |
| 22 | `video` | `VideoSchema` | `youtube_url` (**req**, `url`), `description` | `forms/video.blade.php:3` | `iframe` com `src` = URL crua (não converte para `/embed/`), sem cabeçalho; `id` vem de `$data['id']` (nunca preenchido) (`render/video.blade.php:10-18`) | **nenhum** (não registrado) |

## 8. Widgets de módulo

### 8.1 Homepage (`MOD-LAYOUTS-HOMEPAGE`)

| slug | título | schema | form/render | mídia (`getAttachmentIds`) |
|---|---|---|---|---|
| `sucesu-hero` | Hero SUCESU | `SucesuHeroSchema` | `v2-homepage::lse.forms.hero` / `...render.hero` | `slides.{i}.image` |
| `sucesu-services` | Serviços | `SucesuServicesSchema` | `...forms.services` / `...render.services` | `items.{i}.icon` \( [I] conforme semente `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:266-274` \) |
| `sucesu-about` | Sobre SUCESU | `SucesuAboutSchema` | `...forms.about` / `...render.about` | `image`, `counters.{i}.icon` |
| `sucesu-brands` | Marcas/Associados | `SucesuBrandsSchema` | `...forms.brands` / `...render.brands` | `items.{i}.image` \( [I] \) |
| `sucesu-events` | Eventos | `SucesuEventsSchema` | `...forms.events` / `...render.events` | `items.{i}.image` \( [I] \) |
| `sucesu-regionais` | Regionais | `SucesuRegionaisSchema` | `...forms.regionais` / `...render.regionais` | `image` |
| `sucesu-news` | Notícias | `SucesuNewsSchema` | `...forms.news` / `...render.news` | `items.{i}.image` \( [I] \) |
| `sucesu-associados` | Associados | `SucesuAssociadosSchema` | `...forms.associados` / `...render.associados` | \( [I] ver widget \) |
| `sucesu-noticias` | Notícias (variante) | `SucesuNoticiasSchema` | `...forms.noticias` / `...render.noticias` | \( [I] \) |
| `sucesu-agenda` | Agenda Executiva | `SucesuAgendaSchema` | `...forms.agenda` / `...render.agenda` | nenhum (dados do módulo Agenda) |

Registro em `V2/Modules/Layouts/Homepage/ModuleProvider.php:22-31`. `HomepageDataIntegrator` substitui `items` quando `source_type` é `dynamic-events`, `dynamic-news` ou `dynamic-partners`, e mescla dados em `sucesu-agenda` (`V2/Modules/Layouts/Homepage/Services/HomepageDataIntegrator.php:23-51`). Isso é responsabilidade do **módulo**, não do LSE.

### 8.2 Sobre Nós (`MOD-INSTITUCIONAL-SOBRENOS`)

| slug | título | schema | form/render | mídia |
|---|---|---|---|---|
| `sobre-nos-about` | Sobre Nós — Sobre | `SobreNosAboutSchema` | `v2-sobrenos::lse.forms.about` / `...render.about` | \( [I] IDs múltiplos \) |
| `sobre-nos-team` | Sobre Nós — Diretoria | `SobreNosTeamSchema` | `...forms.team` / `...render.team` | nenhum (dados de `BoardMember` via `SobreNosDataIntegrator`) |
| `sobre-nos-video` | Sobre Nós — Vídeo | `SobreNosVideoSchema` | `...forms.video` / `...render.video` | \( [I] `background_image_id` \) |
| `sobre-nos-counters` | Sobre Nós — Contadores | `SobreNosCountersSchema` | `...forms.counters` / `...render.counters` | \( [I] ícones \) |
| `sobre-nos-testimonial` | Sobre Nós — Depoimento | `SobreNosTestimonialSchema` | `...forms.testimonial` / `...render.testimonial` | `image`, `client_image`, `icon`, `shape_1`, `shape_2` |

Registro em `V2/Modules/Institucional/SobreNos/ModuleProvider.php:20-24`. O portal filtra `sobre-nos-testimonial` (`V2/Modules/Institucional/SobreNos/Controllers/SobreNosPortalController.php:33-35,95-104`) e hidrata `sobre-nos-team` com o biênio corrente (`SobreNosDataIntegrator.php:39-73`).

### 8.3 Como o registry resolve colisões e ordem

- **Colisão padrão × módulo**: o padrão só é registrado se `!has($type)` (`LandingSectionsEditorRendererService.php:63`). Portanto o módulo vence se registrar antes do primeiro `getWidgets()`, o que acontece no construtor do provider.
- **Colisão módulo × módulo**: `WidgetRegistry::register` atribui sem checar (`WidgetRegistry.php:19-21`). O último `ModuleProvider` construído vence; a ordem é a de `File::directories('V2/Modules/<Grupo>')`, sem `sort` explícito (`V2/Core/Providers/V2ServiceProvider.php:92-105`). `[I]` a ordem real depende do filesystem; não há garantia estável entre ambientes.
- **Ordem de exibição**: `all()` devolve o array PHP na ordem de inserção (`WidgetRegistry.php:40-43`). Como os módulos registram no boot e os padrões são anexados depois, os tipos de módulo aparecem antes dos padrões no array; o dropdown, porém, os separa por prefixo (“Personalizados” depois de “Padrão”) (`Views/components/forms/landing-sections-editor.blade.php:35-70`).
- **Dentro de “Padrão”**, a ordem é a de `$legacyConfigs` (`LandingSectionsEditorRendererService.php:38-60`).
- **Dentro de “Personalizados”**, a ordem é a de registro dos módulos.

## 9. Consumidores e dependências

| Relação | Ator | O que usa do catálogo | Risco de quebra |
|---|---|---|---|
| Consumidor | `MOD-LAYOUTS-HOMEPAGE` | 10 widgets `sucesu-*`, schemas e views próprios | Alto: slugs e `parameters` fazem parte do payload salvo |
| Consumidor | `MOD-INSTITUCIONAL-SOBRENOS` | 5 widgets `sobre-nos-*` + `image-picker` do LSE | Alto: filtro por slug e normalização de ids |
| Dependência (LSE → CRE) | `SVC-CONTENTRICHEDITOR` | O LSE usa campo rico nos forms (`boxes`, `carousel`, `grid`, `icons-text`, `multi-card`, `questions-answers`, `step-by-step`, `text-carousel`, `text-image`, `text-only`, `two-columns`) e `toHtml` nos renders; o CRE **não** conhece o LSE | Médio: mudança no formato JSON do campo rico quebra os forms/renders do LSE |
| Dependência (LSE → Gallery) | `SVC-GALLERY` | `image_id`/`image-picker` e `v2_gallery_media`; o LSE delega upload/edição via `GallerySender` | Médio: IDs são persistidos nos parâmetros |
| Dependência (LSE → Storage) | `SVC-STORAGE` | `StorageService::url` no thumbnail do `image-picker` e nas views de módulo | Baixo |

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| LG-LSE-08 | Não há teste por tipo (schema/form/render); só `hero` é exercitado. | Regressões silenciosas em 20 tipos. | `tests/Feature/V2/Services/LandingSectionsEditor/LandingSectionsEditorTest.php:122-146` |
| LG-LSE-09 | `[I]` os `getAttachmentIds` de `sucesu-services/brands/events/news/noticias/associados/regionais` não foram confirmados individualmente além dos dois lidos; os campos foram inferidos da semente e dos renders. | Contrato de mídia incompleto no doc. | `V2/Modules/Layouts/Homepage/LseWidgets/Widgets/` |
| LG-LSE-10 | Não há catálogo de ícones válidos; forms usam listas `$friendlyIcons` locais e selects. | Valores fora da lista podem quebrar o ícone sem erro. | `Views/forms/boxes.blade.php:26-49` |
| LG-LSE-11 | `video` sem decisão de produto. | Tipo órfão. | `Views/render/video.blade.php` |

## 11. Checklist de paridade

- [ ] `PC-LSE-16` — No boot normal (Homepage + Sobre Nós), `getSectionTypes()` devolve **36 slugs**: 15 de módulo (10 `sucesu-*` + 5 `sobre-nos-*`), na ordem de registro dos providers, seguidos de 21 padrões na ordem de `$legacyConfigs`. Sem widgets de módulo, devolve apenas os 21 padrões. O dropdown separa por prefixo (`sucesu-`/`sobre-nos-` → “Personalizados”). Fonte: `LandingSectionsEditorRendererService.php:38-73,81-105`; `V2/Modules/Layouts/Homepage/ModuleProvider.php:22-31`; `V2/Modules/Institucional/SobreNos/ModuleProvider.php:20-24`.
- [ ] `PC-LSE-17` — `sucesu-*` e `sobre-nos-*` aparecem em “Personalizados”; os demais em “Padrão”. Fonte: `Views/components/forms/landing-sections-editor.blade.php:35-70`.
- [ ] `PC-LSE-18` — `BaseWidget::inferType()` converte `SucesuHeroWidget` em `sucesu-hero`. Fonte: `Widgets/BaseWidget.php:154-159`.
- [ ] `PC-LSE-19` — `getRulesFor('cards-numbered')` devolve as regras de `CardsNumberedSchema`, inclusive `required_if` de `media_type`. Fonte: `ValidationSchemas/ValidationService.php:44-50`; `CardsNumberedSchema.php:14-16`.
- [ ] `PC-LSE-20` — `getRulesFor('sucesu-hero')` sem registro devolve `[]`. Fonte: `ValidationService.php:39-51`.
- [ ] `PC-LSE-21` — `prepareSections()` injeta `image`/`image_variants` para `sucesu-hero`. Fonte: `SucesuHeroWidget.php:29-58`; `LandingSectionsEditorRendererService.php:185-197`.
- [ ] `PC-LSE-22` — `interactive-table` interpreta `table_content` como TSV e usa a primeira linha como cabeçalho quando `has_header`. Fonte: `Views/render/interactive-table.blade.php:21-30,101-111`.
- [ ] `PC-LSE-23` — `questions-answers` fecha os outros itens ao abrir um (`toggleFaq`). Fonte: `Views/render/questions-answers.blade.php:96-119`.
- [ ] `PC-LSE-24` — `html-free` renderiza em iframe `srcdoc` com `sandbox` restrito. Fonte: `Views/render/html-free.blade.php:62-70`.

## 12. Evidências consultadas

- `V2/Services/LandingSectionsEditor/Widgets/{WidgetContract.php,BaseWidget.php,GenericWidget.php}`
- `V2/Services/LandingSectionsEditor/Services/{WidgetRegistry.php,LandingSectionsEditorRendererService.php}`
- `V2/Services/LandingSectionsEditor/ValidationSchemas/` (22 schemas + `ValidationService.php`)
- `V2/Services/LandingSectionsEditor/Views/forms/` (22 arquivos)
- `V2/Services/LandingSectionsEditor/Views/render/` (22 arquivos)
- `V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:1-158`
- `V2/Modules/Layouts/Homepage/{ModuleProvider.php,LseWidgets/**,Services/HomepageDataIntegrator.php}`
- `V2/Modules/Institucional/SobreNos/{ModuleProvider.php,LseWidgets/**,Services/SobreNosDataIntegrator.php,Controllers/SobreNosPortalController.php}`
- `V2/Core/Providers/V2ServiceProvider.php:88-118`
- `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:241-394`; `V2/Modules/Institucional/SobreNos/Seeds/SobreNosDefaultSectionsSeeder.php:41-131`
