# 01 — Elementor Clássico (não-atómico): páginas, layout, widgets, templates, globals, custom code, composite Fonte: leitura directa do código-fonte `emcp-tools` v3.12.1 (build Free), instalado em `emanuelalmeida.pt` (`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`), 19-08-2026. Cobre as classes de abilities que operam sobre o modelo de dados Elementor **clássico** (`_elementor_data`, árvore `container`/`widget`/`section`/`column`), incluindo os grupos de leitura/escrita das **Global Classes** do Elementor 4.0+ (que já não são "clássicas" mas vivem no mesmo `_elementor_data`/kit e não fazem parte do sistema de props atómicas tratado no doc 02). Todas as classes aqui descritas registam-se dentro do bloco `if ( $elementor_active )` de `class-ability-registrar.php` (linha ~452), sem nenhum outro gate de módulo — só o Elementor (free) precisa de estar activo. ## ⚠️ Nota importante para a batch — onde vive o CRUD de widgets custom `class-widget-abilities.php` (`EMCP_Tools_Widget_Abilities`) **NÃO contém** nenhuma tool de criação/edição/eliminação de widgets custom (`create-custom-widget`, `update-custom-widget`, `get-custom-widget`, `list-custom-widgets`, `set-widget-status`, `delete-custom-widget` — a lista de 16 tools "Widget/Block Builder" identificada em `skill://emcp-tools` §2.14). Esta classe cobre **só colocação/actualização de instâncias de widget numa página** — três tools: `add-free-widget`, `add-pro-widget` (ambas catalog-backed, inserem um widget *já existente* no registo do Elementor) e `update-widget` (edita definições de uma instância já colocada). O CRUD de definição de widgets custom (a "fábrica" que cria um NOVO tipo de widget PHP/JS a partir de um spec) vive noutro módulo — **confirmado por grep ao `class-ability-registrar.php`, não faz parte de nenhuma das 10 classes desta tarefa** — quase certamente no sandbox de widgets/blocos custom tratado no doc 06 (`EMCP_Tools_Sandbox_*`). Doc06 deve confirmar isto ao ler o registrar completo; aqui fica o achado negativo registado para não haver dupla cobertura nem lacuna. --- ## 1. `EMCP_Tools_Page_Abilities` — `includes/abilities/class-page-abilities.php` **Condição de registo:** dentro de `if ( $elementor_active )`, sempre (sem gate adicional). Construtor recebe `EMCP_Tools_Data $data` e `EMCP_Tools_Element_Factory $factory` (injectados pelo registrar). 5 tools, todas prefixadas `emcp-tools/`. | Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive | |---|---|---|---|---| | `create-page` | `title*` (string), `status` (`draft`\|`publish`, default draft), `post_type` (`page`\|`post`, default page), `template` (slug), `content` (array de elementos, opcional) | `wp_insert_post()` com `_elementor_edit_mode=builder` + `_elementor_template_type=wp-{post_type}`; grava `content` (ou `[]` se omitido) via `EMCP_Tools_Data::save_page_data()`; devolve `post_id`, `edit_url`, `preview_url`. | `check_create_permission`: `publish_pages` \|\| `edit_pages` | não-readonly, não-destructive, não-idempotente | | `update-page-settings` | `post_id*`, `settings*` (objecto livre) | Delegado 1:1 a `EMCP_Tools_Data::save_page_settings()` — grava definições ao nível de página (background, padding, custom CSS, layout) via `Document::save(['settings'=>...])` com fallback a merge em `_elementor_page_settings`. | `check_edit_permission`: `edit_posts` + (se `post_id`) `edit_post` desse post | não-readonly, não-destructive, idempotente | | `delete-page-content` | `post_id*` | `save_page_data($post_id, [])` — **limpa TODO o conteúdo Elementor da página**, mantendo a página em si (post continua a existir). | `check_delete_permission`: `edit_posts` **E** `delete_posts`, mais (se `post_id`) `edit_post` **E** `delete_post` desse post — a única classe do doc que exige capability de eliminação para uma operação que tecnicamente só edita meta, porque o efeito é irreversível sem o change-ledger | não-readonly, **destructive**, idempotente | | `import-template` | `post_id*`, `template_json*` (array de elementos Elementor), `position` (default -1 = append) | Lê a página actual, `reassign_ids()` a todo o `template_json` (evita colisão de IDs), insere no array (append ou `array_splice` na posição) e grava. Devolve `elements_count` (contagem recursiva via `count_elements()`). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `export-page` | `post_id*` | Devolve `get_page_data($post_id)` verbatim como `json` — export completo da árvore Elementor da página, reimportável via `import-template`/`apply-template`. | `check_edit_permission` | **readonly**, idempotente | **Achado de design:** `create-page` grava sempre `_elementor_data` mesmo quando `content` é omitido (`save_page_data($post_id, [])`) — isto **inicializa** a meta em vez de a deixar por criar, o que é relevante porque `EMCP_Tools_Data::get_page_data()` trata "meta ausente" e "meta `[]`" da mesma forma (array vazio), mas só a segunda garante que o Elementor reconhece a página como "Editada com Elementor" desde o primeiro save. --- ## 2. `EMCP_Tools_Layout_Abilities` — `includes/abilities/class-layout-abilities.php` **Condição de registo:** dentro de `if ( $elementor_active )`, sempre. Mesmo par de dependências injectadas (`$data`, `$factory`). 9 tools — o grupo com mais ferramentas do Elementor clássico, cobrindo toda a manipulação estrutural da árvore de elementos. | Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive | |---|---|---|---|---| | `add-container` | `post_id*`, `parent_id` (vazio = topo), `position` (-1=append), `settings` (flex/grid completo), `full_bleed` (bool) | Cria um `container` via `EMCP_Tools_Element_Factory::create_container()` e insere na árvore. `full_bleed=true` faz merge do preset `full_bleed_preset()` (content_width=full, width 100%, padding/gap zero, column+stretch) **antes** de aplicar `settings` do chamador (que sempre ganham). **Bloqueia** com erro accionável se `EMCP_Tools_Atomic_Props::is_container_supported()` for falso (ver "Gotchas" abaixo). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `update-container` | `post_id*`, `element_id*`, `settings*` (merge parcial) | Valida que o elemento alvo é mesmo um container (`is_container_type()`) antes de aplicar `update_element_settings()` — devolve erro `not_container` se for widget (aponta para `update-widget`). | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `update-element` | `post_id*`, `element_id*`, `settings*` | Versão **universal** de update — funciona em qualquer elType, container ou widget, sem o caller precisar de saber qual é. Aceita também `styles`/`editor_settings` no payload (roteados para a raiz do elemento pela camada de dados — ver §Elementor_Data). Recomendado como default em vez de `update-container`/`update-widget` separados. | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `batch-update` | `post_id*`, `operations*` (array de `{element_id, settings}`) | Aplica múltiplos updates **num único save** (uma leitura + uma escrita da página inteira) — muito mais eficiente que N chamadas a `update-element`. Continua a processar mesmo com falhas parciais; devolve `{success, updated, failed:[{element_id,reason}]}`. | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `set-element-label` | `post_id*`, `element_id*`, `title*` | Wrapper de conveniência sobre `update_element_settings()` com `editor_settings.title` — define só o rótulo do Navigator (útil sobretudo em elementos atómicos v4, mas funciona em qualquer elType). | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `reorder-elements` | `post_id*`, `container_id*`, `element_ids*` (ordem desejada) | Reordena os filhos DIRECTOS de um container. Valida que todos os `element_ids` são de facto filhos directos (erro se não). Filhos existentes não mencionados na lista são acrescentados no fim (preservados, não perdidos). | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `move-element` | `post_id*`, `element_id*`, `target_parent_id*` (vazio=topo), `position*` | Remove o elemento da posição actual e reinsere no destino — implementado como `remove_element()` + `insert_element()` sequenciais sobre a mesma árvore em memória, um único save no fim. | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `remove-element` | `post_id*`, `element_id*` | Remove o elemento e **todos os filhos** da árvore. | `check_edit_permission` | não-readonly, **destructive**, idempotente | | `duplicate-element` | `post_id*`, `element_id*` | Clona profundamente o elemento (`reassign_element_ids()` — novos IDs em toda a subárvore, incluindo remapeamento de classes de estilo locais v4 se aplicável) e insere logo a seguir ao original. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | ### Gotchas de design documentados no código (Layout) - **`is_container_type()` inclui os tipos atómicos** (`container`, `e-flexbox`, `e-div-block`) — não é só legado; `update-container`/`reorder-elements` reconhecem containers v4 também (comentário no código refere issues #104/#72 como o mesmo problema raiz). - **`add-container` recusa-se a criar um `container` legado se a experiência "Flexbox Container" do Elementor estiver desligada** (`EMCP_Tools_Atomic_Props::is_container_supported()`). Antes desta guarda (issue #111), o elemento era gravado com sucesso mas o Elementor simplesmente **não o renderiza** em runtime — página fica vazia sem qualquer erro visível ao agente. Este é o tipo de falha silenciosa mais perigosa do plugin: a tool "funciona" (devolve `success:true`) mas o resultado visual é nada. A mesma guarda está em `build-page` (ver §10). - **`full_bleed` preset (#83):** em páginas com template Canvas, os defaults "boxed" do Elementor deixam faixas brancas nas margens de secções full-width (headers/footers). O preset resolve isto de forma reutilizável em vez de o agente ter de descobrir os 6 campos certos por tentativa e erro. --- ## 3. `EMCP_Tools_Widget_Abilities` — `includes/abilities/class-widget-abilities.php` **Condição de registo:** dentro de `if ( $elementor_active )`. `add-pro-widget` só regista se `defined('ELEMENTOR_PRO_VERSION')` (gate interno na própria classe, não no registrar). 3 tools. Construtor recebe `$data`, `$factory`, `$schema_generator` (`EMCP_Tools_Schema_Generator`, usado por `get-widget-schema` noutra classe P0, não aqui) e `$validator` (`EMCP_Tools_Settings_Validator`, usado para validar settings contra o schema do widget alvo). | Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive | |---|---|---|---|---| | `add-free-widget` | `post_id*`, `parent_id*`, `widget_type*`, `position`, `settings` | Valida tier via `EMCP_Tools_Widget_Catalog::is_pro($widget_type)` — **rejeita** (`wrong_tier`) se o tipo pedido for Pro/Woo. Faz merge dos `defaults` do catálogo (`entry['defaults']`) por baixo do `settings` do chamador (chamador sempre ganha). Delega ao motor comum `execute_add_widget()`. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `add-pro-widget` | idem `add-free-widget` | Espelho exacto, mas com o tier invertido — rejeita widgets free (aponta para `add-free-widget`). Só registada quando Elementor Pro está activo (gate natural: não faz sentido oferecer a tool se não há widgets Pro para colocar). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `update-widget` | `post_id*`, `element_id*`, `settings*` | Universal para instâncias de widget já colocadas: encontra o elemento, valida `elType==='widget'` (erro `not_a_widget` caso contrário), faz merge parcial de `settings`. | `check_edit_permission` | não-readonly, não-destructive, idempotente | ### Motor comum: `execute_add_widget()` (privado, partilhado pelas duas tools de inserção) Passos: (1) valida que `widget_type` existe de facto no registo Elementor (`Plugin::$instance->widgets_manager->get_widget_types($widget_type)` — erro `invalid_widget_type` se não), (2) se `settings` não vazio, chama `EMCP_Tools_Settings_Validator::validate($widget_type, $settings)` (mesmo validador de `includes/validators/`, mas para controls de widget — distinto do `Element_Validator` que valida a *forma* estrutural do elemento, ver §Elementor_Data), (3) `factory->create_widget()`, (4) `data->insert_element()`, (5) `data->save_page_data()`. **Achado de design:** o catálogo (`EMCP_Tools_Widget_Catalog`, não lido nesta tarefa — pertence provavelmente ao doc 02 ou doc 10) é a fonte de verdade de tier E de defaults por widget — as duas tools de inserção são finas camadas de gate+merge sobre um motor único; **não há lógica de posicionamento/inserção duplicada entre free e pro**. --- ## 4. `EMCP_Tools_Template_Abilities` — `includes/abilities/class-template-abilities.php` **Condição de registo:** dentro de `if ( $elementor_active )`. `save-as-template` e `apply-template` sempre registadas; as outras 6 só se `defined('ELEMENTOR_PRO_VERSION')` (gate interno, `get_ability_names()` e `register()` espelham a mesma condição). 8 tools no total (2 free + 6 Pro). | Tool | Tier | Input (resumo) | O que faz | `permission_callback` | readonly / destructive | |---|---|---|---|---|---| | `save-as-template` | free | `post_id*`, `element_id` (omitir=página inteira), `title*`, `template_type` (`page`\|`section`\|`container`, default page) | Cria um post `elementor_library` com `_elementor_template_type`, define a taxonomia `elementor_library_type`, grava os elementos (página inteira ou só o elemento indicado) como `_elementor_data` desse novo post-template. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `apply-template` | free | `post_id*`, `template_id*`, `parent_id`, `position` | Lê o template, `reassign_ids()`, insere na página alvo (dentro de `parent_id` ou ao nível de topo). Devolve `elements_added` (contagem via `count_elements()`). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `create-elementor-theme-template` | **Pro** | `title*`, `template_type*` (enum: header/footer/single/single-post/single-page/archive/search-results/error-404/loop-item) | Cria post `elementor_library` do tipo indicado, inicializa com `_elementor_data=[]`. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `set-elementor-template-conditions` | **Pro** | `post_id*`, `conditions*` (array de arrays de partes, ex. `["include","singular","post"]`) | Ver "Gotcha crítico #38" abaixo — usa `save_elementor_conditions()`. | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `list-dynamic-tags` | **Pro** | `group` (filtro opcional) | Enumera `Plugin::instance()->dynamic_tags->get_tags()`, filtra por grupo se indicado, devolve `{name, title, group, categories}` por tag. | `check_edit_permission` | **readonly**, idempotente | | `set-dynamic-tag` | **Pro** | `post_id*`, `element_id*`, `setting_key*`, `tag_name*`, `tag_settings` | Constrói o valor `[elementor-tag id="…" name="…" settings="…"]` (formato interno do Elementor, `settings` urlencoded como JSON) e escreve-o em `element.settings.__dynamic__[setting_key]`, tornando essa definição dinâmica. | `check_edit_permission` | não-readonly, não-destructive, idempotente | | `create-popup` | **Pro** | `title*` | Cria post `elementor_library` tipo `popup`. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente | | `set-popup-settings` | **Pro** | `post_id*`, `triggers`, `conditions`, `timing` | Grava `_elementor_popup_triggers`/`_elementor_popup_timing` em post meta directamente; `conditions` reutiliza o MESMO `save_elementor_conditions()` seguro que o template (não é um caminho separado). | `check_edit_permission` | não-readonly, não-destructive, idempotente | ### Gotcha crítico documentado (#38) — `save_elementor_conditions()` Método privado partilhado por `set-elementor-template-conditions` e `set-popup-settings`. **A abordagem anterior** (`update_post_meta()` + `delete_option()` na cache global `elementor_pro_theme_builder_conditions`) **invalidava a localização de TODOS os templates** sem os reconstruir — definir condições num template partia silenciosamente headers/footers não relacionados até um rebuild completo. A abordagem correcta passa pelo **conditions manager** do próprio Elementor Pro (`ThemeBuilder::get_conditions_manager()->save_conditions()`), que regenera a cache correctamente; só cai para escrita directa de meta (sem tocar na cache global) se o gestor Pro não estiver disponível. **Blueprint:** nunca fazer bypass da API de alto nível de um plugin de terceiros para "poupar uma chamada" quando essa API mantém uma cache side-effectful — o preço é corromper estado partilhado fora do escopo da própria operação. --- ## 5. `EMCP_Tools_Global_Abilities` — `includes/abilities/class-global-abilities.php` **Condição de registo:** dentro de `if ( $elementor_active )`, sempre. Construtor só recebe `$data` (não usa `$factory`). 2 tools — actuam sobre o **kit activo** do Elementor (`Plugin::$instance->kits_manager->get_active_kit()`), que é o post que guarda paleta global de cores e tipografia site-wide. | Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive | |---|---|---|---|---| | `update-global-colors` | `colors*` (array de `{_id*, title*, color*}` hex) | Lê `kit_settings['custom_colors']`, faz merge por `_id` (actualiza existentes, acrescenta novos), `kit->update_settings(['custom_colors'=>...])`. Regista snapshot no change-ledger ANTES de escrever (`snapshot_kit_settings()`/`record_kit_change()` — captura `_elementor_page_settings` do kit para rollback). | `check_manage_permission`: `manage_options` | não-readonly, não-destructive, idempotente | | `update-global-typography` | `typography*` (array de `{_id*, title*, typography_font_family, typography_font_size, typography_font_weight, typography_line_height, typography_letter_spacing}`) | Mesmo padrão merge-por-`_id` sobre `kit_settings['custom_typography']`, mas com **allowlist explícita de chaves** (`allowed_keys`) — qualquer campo fora dessa lista é descartado silenciosamente. Força sempre `typography_typography='custom'` (activa o override; sem isto o Elementor ignora os campos custom). | `check_manage_permission` | não-readonly, não-destructive, idempotente | **Achado de design — a única classe do doc com change-ledger integrado directamente no fluxo de escrita** (não via o hook central de `save_page_data()`, porque estas duas tools não passam por `_elementor_data` nenhuma — escrevem `_elementor_page_settings` do post do kit). `snapshot_kit_settings()` + `record_kit_change()` chamam `EMCP_Tools_Change_Recorder::record_meta()` explicitamente antes do `update_settings()`, porque de outra forma uma mudança de cor/tipografia global — que afecta **todas as páginas do site simultaneamente** — não teria rollback nenhum via o ledger genérico (esse só cobre `_elementor_data` de um post individual, ver `class-elementor-data.php`). **Blueprint:** qualquer mutação "global" que não passe pelo caminho de escrita comum de página precisa do seu PRÓPRIO ponto de integração com o change-ledger — não é automático. --- ## 6. `EMCP_Tools_Global_Classes_Abilities` — `includes/abilities/class-global-classes-abilities.php` **Condição de registo:** classe auto-gated — `is_available()` verifica `class_exists('\Elementor\Modules\GlobalClasses\Global_Classes_Repository')` (Elementor 4.0+). O registrar só instancia se `class_exists('EMCP_Tools_Global_Classes_Abilities')` E chama sempre `register()`, que internamente re-verifica `is_available()`. 1 tool, **read-only**. Resolve a peça mais opaca do sistema de design do Elementor 4.0+: elementos referenciam classes CSS globais só pelo ID opaco `g-xxxxxxx`; sem esta tool, um agente que lê um elemento vê o ID mas não sabe o que ele estiliza. | Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive | |---|---|---|---|---| | `list-global-classes` | `class_ids` (array opcional; omitir = todas) | `Global_Classes_Repository::make()->all()` → normaliza `Collection`/array, para cada item resolve `{id, label, css}` onde `css` é `flatten_variants()` — um mapa `breakpoint[:state] → {prop:valor}` com os `$$type`-wrapped props desembrulhados via `EMCP_Tools_Atomic_Props::unwrap()`. | `check_read_permission`: `edit_posts` | **readonly**, idempotente | ### Gotcha documentado (#57) — resolução defensiva por item Cada item é resolvido dentro do seu próprio `try/catch`. **Antes desta guarda**, uma única classe malformada fazia a enumeração inteira (`resolve-all`, sem `class_ids`) falhar por completo, enquanto pedidos com `class_ids` explícitos (que saltam a entrada má) continuavam a funcionar — inconsistência confusa de diagnosticar ("porque é que só falha quando não passo IDs?"). A correcção devolve a classe problemática mesmo assim, com `css:[]` e um campo `error` explicativo, em vez de a fazer desaparecer da lista ou abortar tudo. **Blueprint:** ao enumerar uma colecção de N itens onde cada um pode ter forma inesperada, isolar cada resolução — nunca deixar um item mau abortar os outros N-1. --- ## 7. `EMCP_Tools_Global_Classes_Write_Abilities` — `includes/abilities/class-global-classes-write-abilities.php` **Condição de registo:** mesmo padrão auto-gated (`is_available()` delega para a classe de leitura se existir, senão verifica a mesma constante `REPOSITORY` directamente). 4 tools — **todas em `emcp_tools_disabled_tools` por omissão** (mutação de CSS partilhado entre todas as páginas do site é tratada como categoria de alto risco, ver `skill://emcp-tools` §2.9/§5). Permissão de escrita: `elementor_global_classes_update_class` (capability própria do Elementor, tipicamente só admin) OU `manage_options`. | Tool | Input (resumo) | O que faz | Destructive | |---|---|---|---| | `create-global-class` | `label*`, `styles` (mapa amigável), `props` (escape-hatch raw `$$type`), `breakpoint` (enum de 7 valores, default desktop), `state` (opcional: hover/focus/…) | Lê o estado actual (`items`, `order`) via `read_state()`, gera um novo ID (`mint_id()` — `g-` + 4 bytes hex aleatórios, sem colisão), constrói o objecto `{id, type:class, label, variants:[{meta, props}]}` combinando `styles` (traduzido via `EMCP_Tools_Atomic_Styles::build_common_props()`+`build_flex_props()`) com `props` raw por cima, escreve com `write_state()`. | não | | `update-global-class` | `id*`, `label`, `styles`, `props`, `breakpoint`, `state`, `replace_variant` (bool) | Localiza a variante pelo par `(breakpoint, state)` (`find_variant_index()`); se não existir, acrescenta uma nova variante; se existir e `replace_variant=false` (default), faz merge dos props na variante existente; se `true`, substitui-a inteira. | não | | `delete-global-class` | `id*`, `confirm*` (deve ser `true`) | Remove do mapa `items` e da lista `order`; **exige `confirm:true`** explicitamente — é a única das 4 a ter esse requisito extra, porque apaga a classe de TODOS os elementos que a usam. | **sim** | | `reorder-global-classes` | `order*` (array de IDs `g-`) | A ordem do Class Manager É a ordem de saída CSS — decide qual classe ganha quando duas se aplicam ao mesmo elemento com a mesma especificidade. IDs omitidos em `order` são acrescentados no fim, na ordem actual — **nenhuma classe pode desaparecer** por um reorder parcial (a "baseline order" é a união de `current_order` + `array_keys(items)`, nunca só o que o chamador mandou). | não | ### Como as escritas persistem — o padrão `read_state()`/`write_state()` Todas as 4 tools passam pelo repositório oficial do Elementor (`Global_Classes_Repository::make()`), nunca por meta directa: lê o mapa completo `id => item` + `order[]`, muta em memória, chama `put($items, $order)` — **o Elementor calcula o diff add/modify/delete internamente** e trata relações + limpeza de uso. `write_state()` faz best-effort de espelhar também para o contexto de preview (`set_preview(true)->put(...)`) — se esse segundo write falhar, é tolerado silenciosamente (só logado com `WP_DEBUG`) porque **o write de frontend é a fonte de verdade**; o preview só afecta o que o editor mostra até recarregar. **Achado de design — reutilização directa dos tijolos atómicos v4:** `build_variant_props()` chama `EMCP_Tools_Atomic_Styles::build_common_props()`/`build_flex_props()` — as MESMAS classes de suporte que o sistema de widgets atómicos (doc 02) usa para construir estilos locais por elemento. Isto significa que "escrever uma Global Class" e "aplicar um estilo local a um elemento atómico" partilham o mesmo motor de tradução `styles amigável → props $$type-wrapped` — não há dois formatos de estilo diferentes no plugin, só dois destinos de armazenamento (classe global partilhada vs. classe local de um elemento). --- ## 8. `EMCP_Tools_Custom_Code_Abilities` — `includes/abilities/class-custom-code-abilities.php` **Condição de registo:** dentro de `if ( $elementor_active )`. `add-custom-js` sempre regista (funciona com Elementor free, via widget HTML); as outras 3 só se `defined('ELEMENTOR_PRO_VERSION')`. 4 tools no total (1 free + 3 Pro). Injecção de código executável — a classe com o perfil de risco mais alto deste doc, com o maior número de comentários de segurança no código-fonte. | Tool | Tier | Input (resumo) | O que faz | `permission_callback` | Destructive | |---|---|---|---|---|---| | `add-custom-css` | **Pro** | `post_id*`, `element_id` (omitir=nível de página), `css*`, `replace` (bool) | CSS por elemento usa o placeholder `selector` como wrapper (substituído pelo Elementor no seu gerador de CSS); grava em `settings.custom_css` do elemento ou em `page_settings.custom_css`. Por omissão faz *append*; `replace=true` sobrescreve. Sanitização: remove tags PHP e `` na árvore da página (não é injecção site-wide, é conteúdo normal da página). Remove qualquer `` que o chamador já tenha incluído (evita duplo-wrap); opcionalmente envolve em `DOMContentLoaded`. | `check_js_permission`: `edit_posts`+per-post **E** `unfiltered_html` — a única tool do grupo a exigir `unfiltered_html` além da capability de edição normal, porque injecta um `