feat(global-classes): Elementor v4 Global Classes write tools (CRUD + apply)#12
Conversation
Add four MCP write abilities for Elementor 4.0+ Global Classes in a new Elementor_MCP_Global_Classes_Write_Abilities class, gated on manage_options and self-guarded on the Global_Classes_Repository: - create-global-class: mint g-<7hex> id, wrap ergonomic CSS-prop->value styles into atomic $$type props, validate against Style_Schema when present (schema-in-error on unknown props / type mismatch), build base + variant entries, put + mirror kit preview meta + clear cache. - update-global-class: preserve id; replace only the base/desktop variant when styles given (other variants kept); replace matching breakpoint/state variants; patch label. - delete-global-class: remove from items + order (no cascade on elements). - apply-global-class: append class to an atomic element's settings.classes (dedupe no-op if present); reject non-atomic elements with compact schema-in-error. Wiring: require in elementor-mcp.php, register after the read group in the ability registrar, add a Global Classes group to the admin Tools catalog inside the atomic block (no 'pro' badge -> enabled by default), bump to 1.14.0 across header/constant/readme + CHANGELOG. Tests: capability test (manage_options gate + 4-tool registry) and functional test driving an in-memory Global_Classes_Repository stub (declared in bootstrap so it wins over the capability suites' bare eval). 539 -> 555 tests, all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef745338b8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return $base_props; | ||
| } | ||
|
|
||
| $variants = array( $this->build_variant( null, null, $base_props ) ); |
There was a problem hiding this comment.
Store desktop breakpoint as a string
When create-global-class creates the base variant, this passes null for meta.breakpoint. Elementor's own style parser requires breakpoint to be present and string-valued (the built-in style variant builder uses desktop for the base variant), so classes created here can fail to round-trip through the Class Manager/REST parser and lose or reject their base styles once Elementor sanitizes or edits them. Use the desktop breakpoint string for the stored variant instead of null.
Useful? React with 👍 / 👎.
| * CSS property names whose ergonomic value is a color. | ||
| */ | ||
| const COLOR_PROPS = array( | ||
| 'color', 'background-color', 'border-color', 'border-top-color', |
There was a problem hiding this comment.
Translate background-color to background
background-color is advertised as an ergonomic CSS prop here, but Elementor 4's style schema represents backgrounds through the structured background prop rather than a top-level background-color. On sites where Style_Schema is loaded, a normal payload like styles: {"background-color":"#fff"} is rejected as invalid_styles; without validation it saves a prop the renderer ignores, so agents cannot author background-color classes through the new tool.
Useful? React with 👍 / 👎.
…-color (Codex P2) - Base variant breakpoint stored as the string 'desktop' instead of null. Elementor's atomic style parser expects a string breakpoint (the editor's own variant builder + this fork's create_local_class use 'desktop'); a null base can fail to round-trip through the Class Manager/REST parser. Reads normalize either way, and update's base-variant matching still resolves it via norm_breakpoint. - Removed 'background-color' from the ergonomic COLOR_PROPS: Elementor v4 represents backgrounds through the structured 'background' prop, not a top-level 'background-color', so it would be rejected by Style_Schema or silently ignored by the renderer. Updated tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16c791a6f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private function put( array $items, array $order ) { | ||
| $repo = self::REPOSITORY; | ||
| try { | ||
| $repo::make()->put( $items, array_values( $order ) ); |
There was a problem hiding this comment.
Use the real repository write contract
On current Elementor 4.x, Global_Classes_Repository::put() is a per-class API (put(string $id, array $value)), not a bulk put($items, $order) call. With this call shape, every create/update/delete path reaches this helper and the first argument is an array, so real sites return write_failed from the caught TypeError and never persist the global class change; the test bootstrap masks this by stubbing a different bulk signature.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified false positive. Elementor core Global_Classes_Repository::put() is bulk, not per-class:
public function put( array $items, array $order )(elementor/elementor modules/global-classes/global-classes-repository.php, current main). The shipping Novamira Pro 1.6.0 also calls $repo->put($items, $order) at all three write sites. The (string $id, array $value) signature does not exist on this class. Keeping the bulk call.
| 'word-spacing', 'border-radius', 'border-width', 'gap', 'row-gap', | ||
| 'column-gap', 'top', 'right', 'bottom', 'left', 'flex-basis', |
There was a problem hiding this comment.
Wrap v4 gap as layout-direction
When a user creates or updates a global class with styles: {"gap": 24}, this list routes gap through wrap_size(), producing a plain $$type: size. The existing v4 atomic builder documents and tests that gap must be layout-direction with row/column sizes (includes/class-atomic-styles.php:92-107, tests/unit/regression/AtomicV4SchemaTest.php:83-89), and flat gap/row-gap/column-gap are dropped, so global classes using one of the most common flex properties will be saved but not render correctly.
Useful? React with 👍 / 👎.
On Elementor v4 atomic, flex gap is the structured layout-direction prop
{row,column} (each a Size); a flat gap Size is dropped by the renderer
(class-atomic-styles.php). Removed gap/row-gap/column-gap from the ergonomic
SIZE_PROPS so they aren't silently mis-wrapped — a gap passed anyway now
falls through to a string $$type and is rejected by Style_Schema (honest
failure). Structured gap support is a follow-up.
(Also: the P1 'per-class put()' finding was a verified false positive —
Elementor core Global_Classes_Repository::put(array $items, array $order)
is bulk, matching this code and the shipping Novamira Pro 1.6.0; replied on
the thread with the source.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb1cbc5be8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| $pairs = array( | ||
| '_elementor_global_classes_labels' => '_elementor_global_classes_labels_preview', | ||
| '_elementor_global_classes_order' => '_elementor_global_classes_order_preview', | ||
| ); |
There was a problem hiding this comment.
Mirror the 4.0 preview class store
On Elementor 4.0 installs that still use the single _elementor_global_classes store plus _elementor_global_classes_preview, CRUD writes update the default frontend repository but this sync only copies the newer labels/order meta keys. In that context those keys do not contain the class definitions, so a class created or updated through MCP is missing from the editor preview context and applied elements render with stale/no class styling until something else refreshes the preview store; mirror _elementor_global_classes as well or write through the repository's preview context.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified false positive against Elementor core. Global_Classes_Repository::put() (default frontend context) already synchronizes the preview store itself: put_to_posts() clears the preview metadata for touched items and calls set_preview(true) on the order object, so after a write the touched classes have no stale preview override and the editor reads the current frontend definition (META_KEY_FRONTEND=_elementor_global_classes, META_KEY_PREVIEW=_elementor_global_classes_preview, Has_Preview_Context). Manually copying _elementor_global_classes → _elementor_global_classes_preview would fight that touched-item clearing. The labels/order preview mirror we do keep matches the shipping Novamira Pro 1.6.0 and is belt-and-suspenders for the editor list; the definitions need no manual mirror. Not changing.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb1cbc5be8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Fallback: plain string prop (font-weight, text-align, display, ...). | ||
| return Elementor_MCP_Atomic_Props::string( (string) $value ); |
There was a problem hiding this comment.
Wrap numeric style props before validation
When a global class uses numeric-only CSS properties such as z-index, order, or column-count, those names are not in COLOR_PROPS or SIZE_PROPS, so they hit this fallback and are cast to $$type: string. On Elementor installs where Style_Schema is loaded, validate_against_schema() rejects these otherwise valid styles as type mismatches; without the schema they are persisted with the wrong atomic type, so common numeric class styles cannot be authored through create-global-class or update-global-class.
Useful? React with 👍 / 👎.
Props like z-index / order / flex-grow / column-count are the atomic number prop type, but fell through to the string fallback — rejected by Style_Schema (type mismatch) or persisted with the wrong type. Added NUMBER_PROPS + a number-wrapping branch (ints stay ints; numeric strings coerce; non-numeric falls through to string for an honest schema mismatch). +1 test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cdfe1bdcd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private function put( array $items, array $order ) { | ||
| $repo = self::REPOSITORY; | ||
| try { | ||
| $repo::make()->put( $items, array_values( $order ) ); |
There was a problem hiding this comment.
Write Global Classes into the preview context
When a class is created/updated/deleted from the editor, this only calls put() on the repository's default (published/frontend) context; Elementor keeps Global Classes in separate frontend and preview contexts (set_preview(true) for draft/preview, per the Elementor data-structure docs). The later sync_preview_context() copies only kit labels/order, not the per-class preview JSON/post lookup, so the editor canvas can show stale or missing CSS for newly written classes until something else syncs the preview context.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Underlying observation is fair, but declining the "write both contexts" prescription — it introduces a regression. Rationale:
- Clobber risk: the preview context (
_elementor_global_class_data_preview) is the editor draft. Writing it from a headless MCP call would overwrite a users unpublished in-editor Global Class changes. Frontend-context writes respect the publish boundary. - Precedent: the shipping Novamira Pro 1.6.0 writes frontend-only + mirrors labels/order (exactly what we do). If frontend-only broke editor visibility, their flagship design-system feature would be broken.
- Behavior: changes are live on the published frontend immediately and load in the editor on next open; an already-open editor needs a refresh — standard for any external edit. The Elementor docs explicitly do not document a reconciliation requirement either way.
Documented this as a deliberate context decision in the class docblock. Keeping frontend-context writes.
…decision (Codex P2) Writes use the repository default (frontend/published) context, not the editor preview context — matching the shipping Novamira Pro reference and respecting the publish boundary (a preview write would clobber a user's unpublished in-editor Global Class draft). Changes are live on the frontend immediately and load in the editor on next open. Documented in the class docblock; declined the 'write both contexts' suggestion as a draft-clobber regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f87a77ec50
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| try { | ||
| $ctx = $repo::make()->all(); | ||
|
|
||
| $items = method_exists( $ctx, 'get_items' ) ? $ctx->get_items() : ( is_array( $ctx ) && isset( $ctx['items'] ) ? $ctx['items'] : $ctx ); |
There was a problem hiding this comment.
Guard array contexts before method_exists
When a repository implementation returns the array shape that this fallback is trying to support from all(), this calls method_exists($ctx, ...) before checking is_array($ctx). On PHP 8+, method_exists() throws a TypeError for arrays, which is caught as read_failed, so create/update/delete fail instead of using the array fallback; guard with is_object($ctx)/is_string($ctx) first, and apply the same pattern to the get_order check below.
Useful? React with 👍 / 👎.
…te (Codex P2) On PHP 8, method_exists() TypeErrors on an array argument (param type object|string), so the array-shape fallback in read_state() was dead — it threw before reaching the is_array branch and was swallowed as read_failed. Guard both the get_items and get_order checks with is_object($ctx) first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b11d1e405e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 'label' => $label, | ||
| 'variants' => $variants, | ||
| ); | ||
| $order[] = $id; |
There was a problem hiding this comment.
Enforce the Elementor class cap before creating
When the site is already at Elementor's Global Classes limit, this path still appends another id and writes directly through the repository, bypassing the editor REST API's MAX_ITEMS validation. That can leave the class set above the supported limit, after which the Elementor editor's own global-classes save calls fail until a class is removed; mirror the limit check before adding to $order.
Useful? React with 👍 / 👎.
| private function put( array $items, array $order ) { | ||
| $repo = self::REPOSITORY; | ||
| try { | ||
| $repo::make()->put( $items, array_values( $order ) ); |
There was a problem hiding this comment.
Preserve preview drafts when writing classes
Each create/update/delete call passes the complete map from read_state() into Global_Classes_Repository::put(). In Elementor's frontend context, put() treats every existing id in that full map as modified and clears preview data for modified classes, so creating or deleting one MCP class can discard unrelated unpublished Class Manager edits that are still in preview; use a change-aware write path or limit the mutation to touched ids.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified false positive against Elementor core. Global_Classes_Repository::put() computes a diff, not a wholesale touch:
$changes = [
"added" => array_values( array_diff( $new_ids, $current_ids ) ),
"deleted" => $deleted_class_ids,
"modified" => array_values( array_intersect( $new_ids, $current_ids ) ),
];and preview clearing is applied only to added ∪ updated ∪ deleted. Unchanged ids in the full map are excluded from the diff and their preview drafts are left intact. So passing the complete map (as this code and the shipping Novamira Pro 1.6.0 both do) does not discard unrelated unpublished Class Manager edits. Not changing.
…e (Codex P2) Elementor V4 caps Global Classes (100 currently; 50 in alpha, GitHub #35864) at its REST/save layer, not in the repository's put() — so a direct write can push past it and then wedge the editor's own Class Manager saves until a class is removed. create-global-class now refuses up front when the count is at the cap, with a clear error. The limit reads Elementor's own constant when a build exposes one, defaults to 100, and is filterable (elementor_mcp_global_classes_limit) for forward-compat. +1 test. (Also: the paired 'preview drafts clobbered' P2 was a verified false positive — Elementor's put() diffs added/updated/deleted and clears preview only for those, leaving unchanged classes' preview drafts intact; replied with source.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57cf6c7f5f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * rejected by Style_Schema / persisted with the wrong type. | ||
| */ | ||
| const NUMBER_PROPS = array( | ||
| 'z-index', 'order', 'flex-grow', 'flex-shrink', 'opacity', |
There was a problem hiding this comment.
When a caller creates or updates a class with an opacity style, this list makes wrap_value() persist it as $$type:number. Elementor 4's atomic style schema treats opacity as a Size/percent prop, so schema validation rejects the style when available, and the renderer can drop it when validation is absent; the new tools therefore cannot author a common opacity rule.
Useful? React with 👍 / 👎.
| ); | ||
| foreach ( $pairs as $source => $target ) { | ||
| $value = get_post_meta( $kit_id, $source, true ); | ||
| update_post_meta( $kit_id, $target, $value ); |
There was a problem hiding this comment.
Don't overwrite preview global-class drafts
Every create/update/delete calls this helper, and this write unconditionally replaces the kit's _preview labels/order with the published values. If a user has unpublished Global Class edits in an open Elementor editor, running any of these MCP writes will clobber those preview drafts even though the repository write itself targets the frontend context.
Useful? React with 👍 / 👎.
…ew mirror (Codex P2 x2) - opacity: its atomic prop type (Size/percent vs plain number) is version- dependent and not confidently determinable, so it was mis-typed as number. Removed from the ergonomic lists — passed anyway it falls through to string and the schema surfaces an honest mismatch, rather than persisting a wrong type the renderer drops. - preview mirror: sync_preview_context() unconditionally copied the kit's frontend labels/order into the _preview meta after every write. That both (a) clobbered the preview drafts of unrelated classes a user was editing, and (b) is redundant/counter-productive — Elementor's own put() already reconciles the preview context for touched ids by CLEARING their preview overrides so they fall back to the frontend overlay; re-populating preview from frontend defeats that fallback for other classes. Removed the mirror (and the now-dead active_kit_id helper); rely on put()'s own preview sync. Documented the reasoning in the class docblock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0744f60a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private function put( array $items, array $order ) { | ||
| $repo = self::REPOSITORY; | ||
| try { | ||
| $repo::make()->put( $items, array_values( $order ) ); |
There was a problem hiding this comment.
Limit repository writes to touched classes
When the site already has global classes and an editor has unpublished preview changes to any unrelated class, this helper passes the full $items map from read_state() into Elementor's repository for every create/update/delete. Elementor's current Global_Classes_Repository::put() derives modified as every existing id present in that map and clears preview overrides for all modified ids on frontend writes, so a one-class MCP change can discard unrelated Class Manager drafts and rewrite every existing class. Please use the repository's touched-item change API where available (or otherwise write only the actual mutation) instead of treating the whole set as modified.
Useful? React with 👍 / 👎.
…not bulk put() (Codex P1) Confirmed against Elementor core source (modules/global-classes/ global-classes-repository.php): put() derives modified = array_intersect( new_ids, current_ids) — every existing id in the passed map, NOT a value diff — and on frontend writes runs bulk_clear_preview_meta(to_update) + clear_preview_labels_for_ids(...) over all of them. So passing the full read-modify-write map on any create/update/delete discarded a user's unpublished in-editor Global Class drafts for unrelated classes and rewrote every class. Switched each mutation to Elementor's touched-item API apply_changes( $touched, $changes, $order), naming only the added/modified/deleted id so only that class's preview override is reconciled; other classes' drafts are left intact. Falls back to put() only on older builds lacking apply_changes() (method_exists guard). Added a stub apply_changes() + a test asserting the touched set and that unrelated classes are preserved across create/update/ delete. (This reverses my earlier incorrect 'false positive' call on the same finding — the bulk-put preview clobber is real; verified from the raw source this time, not a summary.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7579fd5a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ( method_exists( $r, 'apply_changes' ) ) { | ||
| $r->apply_changes( $touched, $changes, array_values( $order ) ); | ||
| } else { | ||
| $r->put( $items, array_values( $order ) ); |
There was a problem hiding this comment.
Match the older repository put() signature
When apply_changes() is absent, this fallback calls put() as a bulk write with ($items, $order), but Elementor 4 builds that only expose put() use the per-class API shape (put( string $id, array $value )). On those supported 4.x installs, create/update/delete will hit the fallback and return write_failed instead of mutating global classes, so the compatibility path needs to call the repository's older per-item/delete/order methods or explicitly fail availability.
Useful? React with 👍 / 👎.
… API exists (Codex P2) Verified the full history of Elementor's global-classes-repository.php: - 2024-11: get/delete/patch (no put) - 2024-11-28 … 2025-01-14: put( string $id, array $value ) [per-class] - 2025-02+: put( array $items, array $order ) [bulk] - 2026-05+: apply_changes( $touched, $changes, $order ) apply_change() supports only the touched-item apply_changes() and the bulk put(array, ...) fallback. On the 2024-11…2025-01 per-class-put() window (and the earlier get/delete/patch builds) the bulk-put fallback would TypeError at call time. is_available() now requires apply_changes() OR a reflection-verified bulk put(array, ...) first parameter, so the write tools don't register on those ancient alphas instead of failing mid-write. (Confirms Codex's earlier per-class-put() observation, which I had wrongly dismissed as a flat false positive after checking only current main.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds 4 MCP tools that let an agent create, update, delete, and apply Elementor v4 Global Classes — closing the biggest remaining page-building gap vs Novamira Pro (which already ships headless global-classes CRUD). The plugin had a read-only
list-global-classes; this adds the write half.New tools (all
manage_options-gated):elementor-mcp/create-global-class— label + ergonomicstylesmap (+ optional responsive/statevariants)elementor-mcp/update-global-class— preserves id (bindings survive); replaces only the base variant, keeps otherselementor-mcp/delete-global-classelementor-mcp/apply-global-class— binds a class to an atomic element; hard-rejects non-atomic targets with schema-in-errorHow it works (from a 3-agent recon of Elementor internals + the Novamira reference)
Global_Classes_Repository::make()->put($items, $order), read-modify-write (a bad single write can't clobber the set)._elementor_global_classes_labels/_ordermeta into their_previewcounterparts, else classes render as rawg-ids with no CSS in the editor canvas (the non-obvious bit Novamira also does).g-<7hex>to match the Elementor editor format.{"color":"#111","padding":24}), wrapped to atomic{$$type,value}via the existingAtomic_Propsprimitives; whenStyle_Schemais present, unknown props / type mismatches are rejected with a schema-embeddingWP_Error(self-correction without a round-trip).files_manager->clear_cache()after each write for CSS regen. Everything guarded withclass_exists/method_existsso it no-ops cleanly off a real Elementor.Gating / defaults
Self-guards on
class_exists(Global_Classes_Repository)(Elementor 4.x atomic). Enabled by default (noprobadge;deletebadged destructive). Wired end-to-end: require + registrar + admin Tools catalog (atomic block) + version 1.14.0.Tests
+16 (4 capability + 12 functional), suite 555 green. Functional tests stub an in-memory repository and assert create/update(base-variant-preserving)/delete/apply(non-atomic rejection)/permission gating.
Notes for review (flagged during implementation)
element_supports_classesis a heuristic (settings hasclasseskey ORe--prefixed type OR hasstylesmap) — no controls-introspection on the raw tree.width:"auto") wrap to a string $$type; if the schema expectssizethis could false-reject — worth confirming against a live v4 site.put()assumes the($items, $order)array signature (matches the Novamira reference call); revisit if real Elementor wants a context object.🤖 Generated with Claude Code