test: Divi 5 design system fixtures and format notes for Etappe 4

Seed script writes four global colors (one relative), one variable per
type, four module presets (one default) and two option group presets plus
the draft page "[Fixture] Designsystem" through Divi's own builder REST
routes; export script saves them to tests/Fixtures/divi/stage4/. Notes
document storage, write functions, side effects and permissions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-30 06:03:54 +02:00
co-authored by Claude Opus 5.5
parent 81da979a15
commit 2e4309cb42
12 changed files with 2396 additions and 0 deletions
@@ -0,0 +1,198 @@
# Etappe 4 – Datenformate des Divi-Designsystems (Divi 5.2.1)
Stand 2026-09-28. Grundlage: Quelltext von Divi 5.2.1 und echte Daten, die über Divis eigene Speicherwege angelegt wurden (Seed-Skript `tests/Integration/divi-seed-stage4-design.php`, Fixtures `tests/Fixtures/divi/stage4/`). Erfüllt Spec §14 („Datenformate Designsystem nur aus Code abgeleitet“) und §15 („Format der Presets/Variablen an echten Daten verifizieren“).
Pfadangaben relativ zu `wp-content/themes/Divi/`. Abkürzung **B5** = `includes/builder-5/server/`. Minifizierte Builder-Dateien liegen in `includes/builder-5/visual-builder/build/`; dort steht statt einer Zeile ein Suchbegriff.
## 0. Grundlagen: wo Divi speichert
`et_get_option()`/`et_update_option()` (`epanel/custom_functions.php:211`, `:272`) haben drei Ziele:
- normale Theme-Option → Schlüssel im Array der Option **`et_divi`** (autoload);
- „Product Setting“ (letzter Parameter `true`) → **eigene Option `et_divi_<name>`**, autoload aus (`:224`, `:288-292`);
- `et_delete_option()` (`:316`) kennt **keine** Product Settings und löscht immer nur den Schlüssel in `et_divi`.
Folge: `GlobalPreset::delete_data()` (B5 `Packages/GlobalData/GlobalPreset.php:149-158`) ruft `et_delete_option(option_name())` auf und entfernt die Option `et_divi_builder_global_presets_d5` **nicht** – nur den statischen Cache. Für Etappe 4 nicht verwenden.
`et_update_option()` aktualisiert zusätzlich das globale `$et_theme_options`; direktes `update_option('et_divi', …)` lässt diesen Cache im selben Request veralten.
## 1. Globale Farben
**Speicherort:** `et_divi['et_global_data']['global_colors']` (normale Theme-Option, kein Product Setting). Die fünf Customizer-Farben liegen **nicht** dort, sondern als `et_divi['accent_color']`, `secondary_accent_color`, `header_color`, `font_color`, `link_color` (Zuordnung `GlobalData::$customizer_colors`, B5 `Packages/GlobalData/GlobalData.php:58-83`).
**Format eines Eintrags** (Schlüssel = ID):
```json
"gcid-q9swzy97df": {
"color": "#5b3a29",
"folder": "",
"label": "[Fixture] Holz dunkel",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
}
```
- ID: `gcid-` + 10 Zeichen `[0-9a-z]` (Builder: `generateVariableId`/`generateNanoUuid`, `global-data.js`, Suche ``S=(e,t)=>e||("colors"===t?`gcid-${_()}``). Ältere Daten können UUIDs haben (`gcid-79d0acb1-…`, Beispiele `GlobalData.php:700-770`). Prüfung serverseitig nur `substr($id, 0, 5) === 'gcid-'` (`:597-603`).
- `status`: `active` | `inactive` | `temporary`. Der Builder **löscht nie**: „Löschen“ setzt `status: inactive` (`global-data.js`, Suche `case"DELETE_GLOBAL_COLOR"`).
- `label`/`folder` werden gespeichert, obwohl der Konverter-Kommentar „not until D6“ sagt (`:159-160`); der Builder liest `label` (Suche `label:t.label||""`).
- `lastUpdated`: Format `Y-m-d\TH:i:s.v\Z`.
- **Relative Farbe** (Farbe aus anderer Farbe): `color` enthält eine Referenz
`$variable({"type":"color","value":{"name":"gcid-q9swzy97df","settings":{"lightness":30}}})$`
mit `settings` aus `hue`, `saturation`, `lightness`, `opacity` (`GlobalData::transform_state_into_global_color_value`, `:181-206`). CSS-Ausgabe: `--gcid-989stx15zc: hsl(from var(--gcid-q9swzy97df) calc(h + 0) calc(s + 0) calc(l + 30));` (`FrontEnd/Module/Style.php:910-947` über `StyleLibrary/Utils/Utils.php:86-130`). Abhängigkeiten werden rekursiv aufgelöst (`GlobalData.php:220-310`, Tiefe max. 10 in `:1336ff`).
**Lesen:** `GlobalData::get_global_colors()` (`:333-354`) liefert Customizer-Farben (neu erzeugt, `folder: customizer`, übersetztes Label, `lastUpdated` = jetzt, `:370-398`) **vor** den gespeicherten Farben; gleichnamige gespeicherte Customizer-IDs werden verworfen. Ergebnis wird statisch gecacht (`$_cached_data`), `set_global_colors()` setzt den Cache zurück, `get_customizer_colors()` hat einen eigenen `static`-Cache ohne Reset.
**Schreiben:** `GlobalData::set_global_colors(array $data, $already_sanitized = false)` (`:650-691`):
1. ohne `$already_sanitized`: `sanitize_global_colors_data()` (`:585-616`) – verwirft Einträge ohne `color` oder ohne `gcid-`, wendet `sanitize_text_field` auf jeden skalaren Wert an; `usedInPosts` wird mit `array_map` behandelt und **muss ein Array sein** (sonst `TypeError`).
2. Customizer-IDs im Payload → `et_update_option(<option_name>, color)` und aus dem Payload entfernt (`:664-674`).
3. **`$already_sanitized = false` ersetzt alle gespeicherten Farben, `true` mischt** (`array_merge`) mit den vorhandenen (`:678-684`).
4. `et_update_option('et_global_data', …)`, dann `ET_Core_PageResource::remove_static_resources('all', 'all', true)` (`:687-690`).
**Builder-Speicherweg:** `POST divi/v1/global-data/global-colors` (`VisualBuilder/REST/RESTRegistration.php:1001-1008`) → Schema-Sanitize `sanitize_global_colors_data` (`GlobalDataController.php:100-108`) → `set_global_colors($colors, true)` (`:71-78`), also **Mischen**. Der Builder schickt dabei die komplette Farbliste **einschließlich der fünf Customizer-Farben** (`global-data.js`, Suche `restRoute:"/divi/v1/global-data/global-colors"`); Entfernen einer Farbe ist über diesen Weg nicht möglich.
**Seiteneffekte:**
- Customizer-Sync ist direkt eingebaut (Schritt 2); ein separater Hook existiert nur für Divi 4 (`includes/builder/functions.php:13205-13237`, `et_global_colors_saved`, mit veralteten IDs `gcid-header-color`/`gcid-font-color`).
- `usedInPosts` pflegt nur der Builder im Browser (`UPDATE_GLOBAL_COLORS_USED_IN_POSTS`); serverseitig wird es nie berechnet. Über PHP angelegte Farben behalten `[]`.
- Einmalige Migration Divi 4 → 5 (`maybe_convert_global_colors_data`, `:526-551`) läuft beim Laden der Builder-Einstellungen (`Framework/Settings/Settings.php:159-162`).
**Rechte:** REST-Route nur `current_user_can('edit_posts')` (`GlobalDataController.php:124-130`); `set_global_colors()` selbst prüft nichts. Zusätzlich verlangt jede Divi-REST-Route den Nonce `X-ET-Nonce` für die Aktion `/divi/v1/<route>--POST` (`Framework/Route/RESTRoute.php:98`, `Framework/Controllers/RESTController.php:158-178`).
## 2. Globale Variablen
**Speicherort:** Product Setting **`et_divi_global_variables`** (eigene Option, autoload aus), gespeichert mit `et_update_option('global_variables', $data, false, '', '', true)` (`GlobalData.php:1016`).
**Gespeichertes Format** (PHP-Arrays):
```json
{
"numbers": { "gvid-f6yq6mayfy": { "id": "gvid-f6yq6mayfy", "label": "[Fixture] Eckenradius", "value": "8px",
"order": "1", "status": "active", "lastUpdated": "…", "variableType": "numbers" } },
"strings": { … }, "images": { … }, "links": { … }, "fonts": { … }
}
```
- Typen: nur `numbers`, `strings`, `images`, `links`, `fonts` (`sanitize_global_variables_data`, `:804-850`). **`colors` wird beim Speichern verworfen** – „Farb-Variablen“ im Variablen-Manager sind die globalen Farben aus Abschnitt 1.
- ID: `gvid-` + 10 Zeichen `[0-9a-z]`; Pflicht: `gvid-`-Präfix und nicht-leeres `label`.
- **Das Feld `id` im Eintrag ist nötig:** die CSS-Ausgabe benutzt `$value['id']`, nicht den Schlüssel (`FrontEnd/Module/Style.php:1002`).
- Sanitize: `value` bei `strings` zeichengetreu (`wp_check_invalid_utf8` + `wp_kses_no_null`), bei `links` `esc_url_raw`, sonst `sanitize_text_field` – dadurch wird `order` zum String (`"1"`). `allowedActions` wird verworfen. Der Builder speichert außerdem `lastUpdated` und `variableType` mit (`global-data.js`, Suche `case"ADD_GLOBAL_VARIABLE"`).
- `status`: `active` | `archived`. Builder-„Löschen“ setzt `archived` und `order: null` (Suche `case"DELETE_GLOBAL_VARIABLE"`), echtes Entfernen nur ohne Label/Wert.
- `numbers` erlaubt beliebige CSS-Ausdrücke (`clamp(1.5rem, 4vw, 2.5rem)` bleibt erhalten).
**Lesen:** `GlobalData::get_global_variables()` (`:896-963`) liefert ein Array, **jede Gruppe als `stdClass`** (`(object) $items`, `:959-962`), die Einträge darin als Arrays; ergänzt `allowedActions` und stellt die Customizer-Schriften `--et_global_heading_font` / `--et_global_body_font` (aus `et_divi['heading_font']`/`['body_font']`, Standard „Open Sans“, `:93-104`, `:415-437`) an den Anfang von `fonts`. Leere Gruppen sind leere `stdClass` (JSON `{}`). Eine beschädigte `fonts`-Gruppe wird still auf `[]` gesetzt (`:925`).
**Schreiben:** `GlobalData::set_global_variables(array $data)` (`:1002-1020`) **ersetzt die komplette Option**. Vorher also vollständig lesen, `stdClass` → Array wandeln, `allowedActions` und die beiden Customizer-Schriften herausnehmen (sonst landen sie in der Option), dann schreiben. `import_global_variables()` (`:1087-1137`) mischt, arbeitet aber mit `stdClass`-Gruppen und schreibt nur `id/label/value/status`.
Danach `remove_static_resources('all', 'all', true)`.
**Rechte:** `set_global_variables()` prüft `current_user_can('edit_theme_options') && et_pb_is_allowed('variables_manager')` und **kehrt sonst ohne Fehler zurück** (`:1003-1005`). Die REST-Route prüft nur `edit_posts` (`GlobalDataController.php:350-356`) und meldet danach Erfolg mit den unveränderten Daten – ein stiller Fehlschlag. `variables_manager` ist ein Recht des Divi-Rollen-Editors (`includes/builder/functions.php:9219-9222`, Auswertung `includes/builder/core.php:1128ff`, Option `et_pb_role_settings`; ohne Eintrag gilt „an“).
**Referenzen in Modulen und Presets:** `$variable({"type":"content","value":{"name":"gvid-…","settings":{}}})$` – für Zahlen/Schriften wird daraus `var(--gvid-…)` (`StyleLibrary/Utils/Utils.php:86-130`), für Strings/Links/Bilder der Wert beim Rendern (Dynamic Content, `Packages/Module/Layout/Components/DynamicContent/DynamicContentGlobalVariableOptions.php`). `:root` enthält nur `numbers` und `fonts` (`Style.php:991-1030`), Schriftwerte in Anführungszeichen. In `post_content` ist `"` innerhalb der Referenz als `"` kodiert (WordPress-Block-Serialisierung).
**Schriften der Customizer-Seite** schreibt der Builder separat: `POST divi/v1/global-data/global-fonts` → `et_update_option('heading_font'|'body_font')` + Cache leeren (`GlobalDataController.php:166-190`, Recht `edit_posts`).
## 3. Presets (Modul- und Optionsgruppen-Presets)
**Speicherort:** Product Setting **`et_divi_builder_global_presets_d5`** (`GlobalPreset::option_name()` = `builder_global_presets_d5`, `GlobalPreset.php:129`), autoload aus. Divi-4-Presets: `et_divi_builder_global_presets_ng` (wird bei Bedarf importiert, Flag `builder_is_legacy_presets_imported_to_d5`).
**Gespeichertes Format:**
```json
{
"module": {
"divi/button": { "default": "", "items": { "3gbmxlurey": { …Modul-Preset… } } },
"divi/divider": { "default": "g7kvfq5uh8", "items": { "g7kvfq5uh8": { … } } }
},
"group": {
"divi/font-body": { "default": "", "items": { "ja6o54jtlc": { …Gruppen-Preset… } } }
}
}
```
Modul-Preset-Eintrag: `type: "module"`, `id`, `name`, `moduleName`, `version` (Builder-Version, hier `"5.2.1"`), `created`/`updated` (**Millisekunden**, Builder `Date.now()`; PHP-Hilfen `_generate_new_preset` nutzen `time()` in Sekunden, `:1695-1707`), `attrs`, optional `styleAttrs`, `renderAttrs`, `groupPresets`, `priority`, `order`.
Gruppen-Preset-Eintrag zusätzlich `groupName` (z. B. `divi/font-body`), `groupId` (Attributpfad im Quellmodul, z. B. `content.decoration.bodyFont`), `moduleName` (Quellmodul), optional `primaryAttrName` (nur über JS-Filter gesetzt, `module-utils.js`, Suche `divi.optionGroupPresetPrimaryAttrNameResolver`).
- **IDs** 10 Zeichen `[0-9a-z]`, ohne Präfix (Builder `generateDefaultPresetName`, `global-data.js`). Namen sind **nicht eindeutig** – der Builder zählt „Button 1, Button 2 …“.
- **`attrs`** = normale Modulattribute (Pfad wie im Block, z. B. `button.decoration.background.desktop.value.color`); Gruppen-Presets tragen die **vollen Pfade des Quellmoduls** und werden beim Anwenden auf andere Module umgerechnet (`GlobalPresetItemGroup::_attrs_resolver`, `GlobalPresetItemGroup.php:245ff`, `GlobalPresetItemGroupAttrNameResolver`).
- **`styleAttrs`/`renderAttrs`**: Teilmengen von `attrs` für CSS bzw. HTML/Skript. Serverseitig berechenbar mit `Conversion::get_preset_attrs($attrs, ['style'] | ['html','script'], Conversion::get_preset_attrs_mapping($moduleName))` (B5 `Packages/Conversion/Conversion.php:2242`, `:2479`; genutzt in `convert_preset`, `:2144-2145`). Das Frontend benutzt `renderAttrs`/`styleAttrs` der Gruppen-Presets direkt (`Packages/ModuleLibrary/ModuleRegistration.php:326-340`).
- **`groupPresets`** im Modul-Preset (verschachtelte Gruppen-Presets): `{ "<groupId>": { "presetId": ["<id>"], "groupName": "divi/…" } }`.
- **`default`** je Modul/Gruppe: ID des Standard-Presets oder `""`. Der Builder legt beim Öffnen des Preset-Managers leere Standard-Presets an (`ADD_DEFAULT_MODULE_PRESET`); ein Standard-Preset **ohne `attrs`** erzeugt keine Klasse (`get_module_preset_class_names`, `GlobalPreset.php:730-800`).
- **`priority`** fehlt meist und gilt dann als `10` (`:676`, `:2078`, `:2761`); das REST-Schema-Default `10` (`GlobalPresetController.php`, `preset_type_properties`) wird für verschachtelte Einträge nicht eingesetzt.
**Verweise in Blöcken** (`post_content`):
- Modul-Preset: `"modulePreset": ["3gbmxlurey"]` – **Array** (Stapel); ein String ist Altformat und wird noch akzeptiert (`normalize_preset_stack`, `:2627-2680`). `""`, `"default"`, `"_initial"` bedeuten „Standard-Preset“ (`is_preset_id_as_default`, `:2691`). Fehlt `modulePreset`, gilt das Standard-Preset des Moduls.
- Gruppen-Preset: `"groupPreset": { "module.decoration.spacing": { "presetId": ["apzj7e17ok"], "groupName": "divi/spacing" } }` (`get_selected_group_presets`, `:2021-2320`; Überlagerung `:2148`).
- Stapel: letztes Element gewinnt bei `get_selected_preset` (`:522-600`); `renderAttrs` werden nach `priority` aufsteigend gemischt (`:626-712`). Verschachtelte und explizite Gruppen-Presets werden gestapelt, nicht ersetzt (`:2148-2163`).
- Welche Gruppen ein Modul hat und wie sie heißen: `GlobalPreset::get_group_preset_default_attr(ModuleRegistration::get_module_settings($name))` (`:2332-2465`), Namensliste `get_default_group_name` (`:2475-2511`). Beispiel `divi/text`: `content.decoration.bodyFont` → `divi/font-body`, `module.decoration.spacing` → `divi/spacing`; `divi/button`: `button.decoration.font` → `divi/font`, `button.decoration.button` → `divi/button`.
**CSS-Klassen im Frontend** (`GlobalPresetItemUtils::generate_preset_class_name`, `GlobalPresetItemUtils.php:43-78`): `preset--module--divi-button--3gbmxlurey`, Standard-Preset `preset--module--divi-divider--default`, Gruppe `preset--group--divi-blurb--divi-spacing--apzj7e17ok`, verschachtelt `preset--group--divi-text--divi-font-body--nested--ja6o54jtlc`. Keine Klassen für `divi/id-classes`, `divi/animation`. Am Fixture geprüft (README in `tests/Fixtures/divi/stage4/`).
**Schreiben:** `GlobalPreset::save_data(array $data)` (`:419-431`) schreibt die **komplette Option ungeprüft**, leert Caches (`remove_static_resources('all','all',true,'all',true)` – Builder-CSS bleibt erhalten) und setzt `self::$_data` zurück. Sanitisierung gibt es nur in `GlobalPreset::prepare_data()` (`:213-268`): wandelt das REST-Format (`{module: [{default, items: [ … ]}], group: …}`, Listen statt ID-Schlüssel) ins Speicherformat, entfernt leere Attribut-Arrays und sanitisiert `attrs`/`renderAttrs`/`styleAttrs` mit `SavingUtility::sanitize_block_attrs` bzw. `sanitize_group_attrs` (`VisualBuilder/Saving/SavingUtility.php:2153`, `:2296`; ohne `unfiltered_html` wird gefiltert).
**Builder-Speicherweg:** `POST divi/v1/global-data/global-preset/sync` (`RESTRegistration.php:1035-1045`) → `GlobalPresetController::sync` (`GlobalPresetController.php:45-187`): optional in Blöcken (Transient 5 min), `prepare_data`, **`validate_preset_count`** (`GlobalPreset.php:335-410`: weniger Presets als gespeichert → Fehler 400 `preset_count_decreased`, außer `actionType` `DELETE_MODULE_PRESET`/`DELETE_OPTION_GROUP_PRESET`), bei Löschaktionen Bereinigung der Divi-4-Option (`remove_preset_from_legacy_option`), dann `save_data`. Der Builder schickt immer den **vollständigen** Bestand.
**Rechte:** `sync_permission()` = `UserRole::can_current_user_use_visual_builder()` (`GlobalPresetController.php:460`, `Framework/UserRole/UserRole.php:38-70`) – nur das Rollen-Recht „Visual Builder verwenden“ (Standard „an“), **kein** `edit_theme_options`. Dazu der Routen-Nonce wie in Abschnitt 1.
**Preset-Historie:** `et_divi_builder_presets_history_meta` (`{index, size}`) und `et_divi_builder_presets_history_item_<n>` gehören zu Divi 4 (`includes/builder/feature/global-presets/History.php:4-10`, Schreiben nur per AJAX `et_builder_save_global_presets_history` mit `switch_themes`, `:75-139`). Der Divi-5-Sync schreibt dort **nichts**; Rückgängig im Builder ist rein clientseitig. Lokal: `{index: -1, size: 0}`. Für Etappe 4 heißt das: eine Wiederherstellung gibt es nur über eigene Snapshots (Spec §7).
## 4. Theme-Optionen (`et_divi`)
Lokal ist **keiner** der Whitelist-Schlüssel gesetzt (Fixture `et-divi-subset.json`) – überall gelten Divi-Standards.
| Gruppe | Schlüssel in `et_divi` | Schreibweg in Divi | Sanitize / Standard (Customizer, `functions.php`) |
|---|---|---|---|
| Schriften | `heading_font`, `body_font`, `primary_nav_font`, `secondary_nav_font`, `all_buttons_font` | Customizer, `global-fonts`-REST (nur heading/body) | `et_sanitize_font_choices`, Standard `none` (`:2484`, `:2557`, `:3398`, `:3522`, `:4996`) |
| | `heading_font_weight`, `body_font_weight` | Customizer | `et_sanitize_global_font_weight`, `500` (`:2509`, `:2582`) |
| Größen | `body_font_size` (14), `tablet_body_font_size`, `phone_body_font_size`, `body_header_size` (30), `tablet_header_font_size`, `phone_header_font_size` | Customizer | `absint` (`:2208-2456`); Tablet/Phone-Standard = nächstgrößerer Wert |
| | `body_font_height` (1.7), `body_header_height` (1) | Customizer | `et_sanitize_float_number` (`:2236`, `:2376`) |
| | `body_header_spacing` (0), `body_header_style` (`''`) | Customizer | `et_sanitize_int_number`, `et_divi_global_font_style` (`:2348`, `:2404`) |
| Layout | `content_width` (1080), `gutter_width` (3), `section_padding` (4), `row_padding` (2) | Customizer | `absint` (`:1824`, `:1852`, `:1927`, `:2011`) |
| Farben | `accent_color`, `secondary_accent_color`, `header_color`, `font_color`, `link_color` | Customizer **und** `set_global_colors` (Abschnitt 1) | `et_sanitize_alpha_color` (`:2534-2665`) |
| Buttons | `all_buttons_*` (19 Schlüssel inkl. `*_hover`, z. B. `all_buttons_bg_color`, `all_buttons_border_radius_hover`) | Customizer | je Feld (`:4843ff`) |
| Logo | `divi_logo` | ePanel „Theme-Optionen“ | Typ `upload`, URL-String (`options_divi.php:81`) |
| SEO | `divi_seo_home_*`, `divi_seo_single_*`, `divi_seo_index_*` | ePanel | Checkboxen `'on'`/`'false'`, Texte, Selects (`options_divi.php:721-894`; Speichern `epanel/core_functions.php:868ff`, Checkbox `:969-970`/`:1049`) |
- Customizer-Einstellungen sind `type => 'option'` mit Schlüssel `et_divi[<name>]` und `capability => 'edit_theme_options'`; Divi prüft zusätzlich das Rollen-Recht `theme_customizer` (`functions.php:1278`). ePanel: `edit_theme_options` (`epanel/core_functions.php:873`) und Rollen-Recht `theme_options` (`functions.php:8834-8846`).
- **Standardwerte weichen voneinander ab:** `secondary_accent_color` hat im Customizer `#8800FF` (`functions.php:2665`), in `GlobalData::$customizer_colors` und im Builder `#2ea3f2` (`GlobalData.php:64-68`, `includes/builder/core.php:7293`). Wer die Builder-Sicht (`get_global_colors()`) zurückschreibt, setzt die Customizer-Farbe damit fest auf `#2ea3f2`. Ebenso Schriften: Customizer-Standard `none`, Builder-Sicht „Open Sans“.
- Caches: nach Customizer-Speichern `remove_static_resources('all','all')` (`core/components/PageResource.php:944`, `:1139-1141`) und `et_pb_force_regenerate_templates` (`includes/builder/core.php:3771`). Für eigene Schreibzugriffe gilt dasselbe.
## 5. Caches
`ET_Core_PageResource::remove_static_resources($post_id, $owner, $force, $slug, $preserve_vb_files, $delete_files)` (`core/components/PageResource.php:1213-1232`) läuft **nur**, wenn `wp_doing_cron()` oder `et_core_security_check_passed('edit_posts')` – ohne angemeldeten Benutzer (z. B. WP-CLI ohne `--user`) ist der Aufruf wirkungslos. Farben und Variablen rufen `('all','all',true)`, Presets `('all','all',true,'all',true)` bereits selbst auf; der in Spec §7 vorgesehene Zusatzaufruf ist harmlos, aber redundant.
In-Request-Caches: `GlobalData::$_cached_data` (Reset bei `set_*`), `GlobalPreset::$_data` und `$_normalized_preset_stack_cache` (Reset in `save_data`/`delete_data`), `static` in `get_customizer_colors()`/`get_customizer_fonts()` und `get_group_preset_default_attr()` (kein Reset).
## 6. Rechte im Überblick
| Weg | Prüfung in Divi |
|---|---|
| REST `global-colors`, `global-fonts`, `global-variables` | `edit_posts` + Routen-Nonce |
| `GlobalData::set_global_variables()` | `edit_theme_options` **und** `et_pb_is_allowed('variables_manager')`, sonst stilles `return` |
| `GlobalData::set_global_colors()`, `GlobalPreset::save_data()` | keine |
| REST `global-preset/sync` | Rollen-Recht `use_visual_builder` + Routen-Nonce |
| Customizer | `edit_theme_options` + Rollen-Recht `theme_customizer` |
| ePanel | `edit_theme_options` + Rollen-Recht `theme_options` |
| `remove_static_resources` | `edit_posts` (oder Cron), sonst wirkungslos |
## 7. Abweichungen und Ergänzungen zu Spec §7
1. **„Variablen … über `GlobalData::set_global_variables`“** – stimmt, aber die Funktion ersetzt die ganze Option, liefert keinen Fehler bei fehlenden Rechten und kennt keinen Typ `colors`. Das Tool muss vorher lesen, `stdClass` → Array wandeln, `allowedActions` und die zwei Customizer-Schriften entfernen und nach dem Schreiben per Lesen prüfen.
2. **„globale Farben … inkl. der fünf Hauptfarben (über `set_global_colors`)“** – die Hauptfarben landen in `et_divi['accent_color']` usw.; der zweite Parameter entscheidet zwischen Ersetzen (`false`) und Mischen (`true`). Für Änderungen einzelner Farben: Mischen; Löschen wie der Builder als `status: inactive`. Hauptfarben nur schicken, wenn sie wirklich geändert werden sollen (Standardwert-Abweichung, Abschnitt 4).
3. **„Presets … per Name“** – Namen sind nicht eindeutig; IDs sind 10-stellige Zeichenketten. Schreiben immer als vollständiger Bestand; `save_data()` sanitisiert nicht → vorher `prepare_data()` (REST-Format) und `validate_preset_count()` wie der Controller verwenden. `styleAttrs`/`renderAttrs` müssen mitgeschrieben werden (Frontend liest sie).
4. **„Voraussetzung: `edit_theme_options`; für Variablen zusätzlich `variables_manager`“** – strenger als Divi selbst (Farben: `edit_posts`; Presets: nur Visual-Builder-Recht). Als bewusste Plugin-Entscheidung beibehalten; für Variablen ist die Doppelprüfung zwingend, weil Divi sonst still nichts tut.
5. **„danach `remove_static_resources('all','all',true)`“** – Divis Setter tun das schon; wirkungslos ohne angemeldeten Benutzer. Für Presets nutzt Divi `preserve_vb_files = true`.
6. **Snapshots** sind die einzige Wiederherstellung: Divi 5 führt für Farben, Variablen und Presets keine serverseitige Historie; die Option `et_divi_builder_presets_history_meta` gehört zu Divi 4.
7. **Theme-Optionen:** `divi_logo` und `divi_seo_*` sind ePanel-Felder (andere Sanitize-Regeln, Checkbox-Werte `'on'`/`'false'`), keine Customizer-Einstellungen. Die übrigen Whitelist-Felder haben die Customizer-Sanitize-Callbacks aus Abschnitt 4 – diese sollten wiederverwendet werden.
8. **Lesepfad des Plugins** (`src/Integrations/Divi/Design/DesignSystem.php`): Farb-„Variablen“ (`variables['colors']`) sind in 5.2.1 immer leer – globale Farben tragen Label und ID selbst. `fonts()` liefert die Builder-Sicht („Open Sans“), nicht den Customizer-Standard `none`.
## 8. Stolpersteine (Kurzliste)
- `get_global_variables()` liefert `stdClass` je Gruppe, gespeichert sind Arrays; leere Gruppe = `{}` im JSON.
- Variableneinträge brauchen das Feld `id`; `order` wird zum String.
- `usedInPosts` muss ein Array sein; wird nur vom Builder gepflegt.
- `created`/`updated` in Millisekunden (Builder) vs. Sekunden (PHP-Hilfen).
- `modulePreset` ist ein Array; Standard-Preset wirkt nur mit `attrs`.
- Sync-Endpunkt verweigert kleinere Preset-Anzahl ohne `DELETE_*`-actionType.
- `GlobalPreset::delete_data()` löscht die Product-Setting-Option nicht.
- Divi-REST-Routen verlangen `X-ET-Nonce` pro Route; direkte PHP-Aufrufe umgehen Nonce, Schema und Rechteprüfung.
- `secondary_accent_color`: Customizer-Standard `#8800FF`, Builder-Standard `#2ea3f2`.
+73
View File
@@ -0,0 +1,73 @@
# Divi-5-Fixtures für Etappe 4 (Designsystem: Farben, Variablen, Presets, Theme-Optionen)
Echte Daten aus Divi 5.2.1, erzeugt am 2026-09-28 auf der lokalen DDEV-Site (`https://wordpress.ddev.site:8443`). Vorher hatte die Site keine eigenen Designdaten: nur die fünf Customizer-Farben (alle auf Divi-Standard, in `et_divi` nicht gespeichert), keine Variablen, keine Presets.
Formate, Quellen mit Zeilenangaben und Stolpersteine: `docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md`.
## Wie die Daten entstanden sind
Skript `tests/Integration/divi-seed-stage4-design.php` (idempotent, findet Einträge über ihr „[Fixture] …“-Label wieder):
```
cd /home/janniclas/Projekte/WordPress
ddev exec wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-seed-stage4-design.php
ddev exec wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage4-fixtures.php
```
Das Seed-Skript schreibt **ausschließlich über die REST-Routen, die der Visual Builder selbst aufruft** – prozessintern per `rest_do_request()` als Administrator, mit Divis Routen-Nonce im Header `X-ET-Nonce`. Damit laufen Divis Argument-Schema, Sanitize-Callbacks, Berechtigungsprüfung und Speicherfunktionen genau wie beim Speichern im Builder:
| Bereich | Route | Divi-Funktion |
|---|---|---|
| Farben | `POST divi/v1/global-data/global-colors` | `GlobalDataController::update_global_colors` → `GlobalData::set_global_colors($data, true)` |
| Variablen | `POST divi/v1/global-data/global-variables` | `GlobalDataController::update_global_variables` → `GlobalData::set_global_variables()` |
| Presets | `POST divi/v1/global-data/global-preset/sync` | `GlobalPresetController::sync` → `GlobalPreset::prepare_data()` → `validate_preset_count()` → `save_data()` |
| Seite | `POST divi/v1/sync-to-server` | `SyncToServerController::update` (wie „Entwurf speichern“) |
Die Seite wurde vorher mit `wp_insert_post()` als leerer Entwurf angelegt und mit `et_builder_enable_for_post()` für den Builder freigeschaltet (wie Etappe 3). Die Aufteilung der Preset-Attribute in `styleAttrs`/`renderAttrs` stammt aus Divis eigener Attribut-Karte (`Conversion::get_preset_attrs_mapping()` / `Conversion::get_preset_attrs()`, derselbe Weg wie beim Import von Divi-4-Presets). IDs sind wie im Builder 10 Zeichen aus `[0-9a-z]` (`generateNanoUuid`, `global-data.js`).
Nicht angefasst: die fünf Customizer-Farben (nicht im Payload, `accent_color` & Co. bleiben ungesetzt), Theme-Optionen, Divi-4-Preset-Historie, alle übrigen Inhalte der Site.
## Angelegte Einträge
| Art | ID | Label/Name | Inhalt |
|---|---|---|---|
| Farbe | `gcid-q9swzy97df` | [Fixture] Holz dunkel | `#5b3a29` |
| Farbe | `gcid-989stx15zc` | [Fixture] Holz hell | relativ: Holz dunkel, `lightness +30` |
| Farbe | `gcid-pgtgy2j1i7` | [Fixture] Salbeigrün | `#7d9b76` |
| Farbe | `gcid-gg4lef6xns` | [Fixture] Sandbeige | `#efe6d8` |
| Variable `numbers` | `gvid-f6yq6mayfy` | [Fixture] Eckenradius | `8px` |
| Variable `numbers` | `gvid-5dec72fwsc` | [Fixture] Abstand Karte | `clamp(1.5rem, 4vw, 2.5rem)` |
| Variable `strings` | `gvid-7uckqqhmrh` | [Fixture] Slogan | `Handwerk aus Meisterhand – seit 1987` |
| Variable `images` | `gvid-qmc3cy1alq` | [Fixture] Werkstattbild | URL eines vorhandenen Bildes der Mediathek |
| Variable `links` | `gvid-nt8f6iz2qh` | [Fixture] Kontaktseite | `https://wordpress.ddev.site:8443/kontakt/` |
| Variable `fonts` | `gvid-d4a5bsz2gt` | [Fixture] Akzentschrift | `Merriweather` |
| Modul-Preset `divi/button` | `3gbmxlurey` | [Fixture] Button Primär | Hintergrund Holz dunkel, Schrift Sandbeige/600, Radius = Eckenradius |
| Modul-Preset `divi/text` | `hsiy1sjxt7` | [Fixture] Text Hinweis | Hintergrund Sandbeige, Radius, Innenabstand = Abstand Karte, **verschachteltes** Gruppen-Preset Fließtext |
| Modul-Preset `divi/blurb` | `vdcl6msnuo` | [Fixture] Blurb Leistung | Titel Akzentschrift/Holz dunkel/700, Rahmen Salbeigrün, Radius |
| Modul-Preset `divi/divider` | `g7kvfq5uh8` | [Fixture] Trenner Standard | **Standard-Preset** des Moduls; Linie Holz hell, 3px |
| Gruppen-Preset `divi/font-body` | `ja6o54jtlc` | [Fixture] Fließtext | aus `divi/text`, `content.decoration.bodyFont`; Akzentschrift, Holz dunkel, 17px/1.8em |
| Gruppen-Preset `divi/spacing` | `apzj7e17ok` | [Fixture] Abstand Karte | aus `divi/blurb`, `module.decoration.spacing`; Innenabstand = Abstand Karte, Außenabstand unten 24px |
| Seite | 3182 | [Fixture] Designsystem | Entwurf; nutzt alle Presets und die Variablen string/link/image |
`divi/divider` wird auf keiner anderen Seite der Site verwendet – das Standard-Preset ändert dort nichts.
## Dateien
| Datei | Inhalt |
|---|---|
| `global-colors.json` | `et_divi['et_global_data']` wie gespeichert und `GlobalData::get_global_colors()` (Customizer-Farben vorn; deren `lastUpdated` wird bei jedem Lesen neu erzeugt) |
| `global-variables.json` | Option `et_divi_global_variables` (dekodiert und als serialisierte Zeile), `GlobalData::get_global_variables()` und die PHP-Typen je Gruppe (`stdClass` – eine leere Gruppe erscheint im JSON als `{}`) |
| `presets.json` | Option `et_divi_builder_global_presets_d5`, `GlobalPreset::get_data()`, die Divi-4-Option `et_divi_builder_global_presets_ng`, Importkennzeichen |
| `presets-history.json` | Divi-4-Preset-Historie (`et_divi_builder_presets_history_meta`, keine Einträge) und die Migrationsflags aus `et_divi` |
| `et-divi-subset.json` | Theme-Optionen der Whitelist aus Spec §7: vorhandene Schlüssel mit Wert, fehlende Schlüssel, wirksame Akzentfarben und Customizer-Schriften |
| `page--designsystem.json` / `.html` | Post-Zeile, alle Meta-Einträge roh, `post_content` byte-genau |
| `manifest.json` | Divi-Version, Dateiliste, Post-ID |
## Prüfung im Frontend
Die Vorschau des Entwurfs (angemeldet, `?page_id=3182&preview=true`) enthielt am 2026-09-28:
- Klassen `preset--module--divi-button--3gbmxlurey`, `preset--module--divi-text--hsiy1sjxt7` mit `preset--group--divi-text--divi-font-body--nested--ja6o54jtlc`, `preset--group--divi-text--divi-font-body--ja6o54jtlc` (explizites Gruppen-Preset), `preset--module--divi-blurb--vdcl6msnuo` mit `preset--group--divi-blurb--divi-spacing--apzj7e17ok` und `preset--module--divi-divider--default` (Trenner ohne `modulePreset` → Standard-Preset);
- CSS-Regeln zu diesen Klassen mit `var(--gcid-…)`/`var(--gvid-…)`;
- `:root` mit `--gvid-f6yq6mayfy: 8px`, `--gvid-5dec72fwsc: clamp(…)`, `--gvid-d4a5bsz2gt: 'Merriweather'` und `--gcid-989stx15zc: hsl(from var(--gcid-q9swzy97df) calc(h + 0) calc(s + 0) calc(l + 30))`;
- aufgelöste Inhalte: Slogan-Text, `href="https://wordpress.ddev.site:8443/kontakt/"`, Bild-URL aus der Bild-Variable.
@@ -0,0 +1,58 @@
{
"option_name": "et_divi",
"present": {},
"absent": [
"heading_font",
"body_font",
"heading_font_weight",
"body_font_weight",
"primary_nav_font",
"secondary_nav_font",
"all_buttons_font",
"body_font_size",
"tablet_body_font_size",
"phone_body_font_size",
"body_header_size",
"tablet_header_font_size",
"phone_header_font_size",
"body_font_height",
"body_header_height",
"body_header_spacing",
"body_header_style",
"content_width",
"gutter_width",
"section_padding",
"row_padding",
"accent_color",
"secondary_accent_color",
"header_color",
"font_color",
"link_color",
"divi_logo"
],
"effective": {
"accent_colors": {
"primary": "#2ea3f2",
"secondary": "#2ea3f2",
"heading": "#666666",
"body": "#666666",
"link": "#2ea3f2"
},
"customizer_fonts": {
"--et_global_heading_font": {
"id": "--et_global_heading_font",
"label": "Überschrift",
"value": "Open Sans",
"status": "active",
"order": 1
},
"--et_global_body_font": {
"id": "--et_global_body_font",
"label": "Fließtext",
"value": "Open Sans",
"status": "active",
"order": 2
}
}
}
}
@@ -0,0 +1,116 @@
{
"stored": {
"option_name": "et_divi",
"key": "et_global_data",
"value": {
"global_colors": {
"gcid-q9swzy97df": {
"color": "#5b3a29",
"folder": "",
"label": "[Fixture] Holz dunkel",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-989stx15zc": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{\"lightness\":30}}})$",
"folder": "",
"label": "[Fixture] Holz hell",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-pgtgy2j1i7": {
"color": "#7d9b76",
"folder": "",
"label": "[Fixture] Salbeigrün",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-gg4lef6xns": {
"color": "#efe6d8",
"folder": "",
"label": "[Fixture] Sandbeige",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
}
}
}
},
"get_global_colors": {
"gcid-primary-color": {
"color": "#2ea3f2",
"folder": "customizer",
"label": "Primärfarbe",
"lastUpdated": "2026-09-28T00:22:52.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-secondary-color": {
"color": "#2ea3f2",
"folder": "customizer",
"label": "Sekundärfarbe",
"lastUpdated": "2026-09-28T00:22:52.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-heading-color": {
"color": "#666666",
"folder": "customizer",
"label": "Überschriftentextfarbe",
"lastUpdated": "2026-09-28T00:22:52.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-body-color": {
"color": "#666666",
"folder": "customizer",
"label": "Fließtextfarbe",
"lastUpdated": "2026-09-28T00:22:52.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-link-color": {
"color": "#2ea3f2",
"folder": "customizer",
"label": "Linkfarbe",
"lastUpdated": "2026-09-28T00:22:52.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-q9swzy97df": {
"color": "#5b3a29",
"folder": "",
"label": "[Fixture] Holz dunkel",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-989stx15zc": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{\"lightness\":30}}})$",
"folder": "",
"label": "[Fixture] Holz hell",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-pgtgy2j1i7": {
"color": "#7d9b76",
"folder": "",
"label": "[Fixture] Salbeigrün",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
},
"gcid-gg4lef6xns": {
"color": "#efe6d8",
"folder": "",
"label": "[Fixture] Sandbeige",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"status": "active",
"usedInPosts": []
}
}
}
@@ -0,0 +1,210 @@
{
"stored": {
"numbers": {
"gvid-f6yq6mayfy": {
"id": "gvid-f6yq6mayfy",
"label": "[Fixture] Eckenradius",
"value": "8px",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "numbers"
},
"gvid-5dec72fwsc": {
"id": "gvid-5dec72fwsc",
"label": "[Fixture] Abstand Karte",
"value": "clamp(1.5rem, 4vw, 2.5rem)",
"order": "2",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "numbers"
}
},
"strings": {
"gvid-7uckqqhmrh": {
"id": "gvid-7uckqqhmrh",
"label": "[Fixture] Slogan",
"value": "Handwerk aus Meisterhand – seit 1987",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "strings"
}
},
"images": {
"gvid-qmc3cy1alq": {
"id": "gvid-qmc3cy1alq",
"label": "[Fixture] Werkstattbild",
"value": "https://wordpress.ddev.site:8443/wp-content/uploads/2026/09/600.jpg",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "images"
}
},
"links": {
"gvid-nt8f6iz2qh": {
"id": "gvid-nt8f6iz2qh",
"label": "[Fixture] Kontaktseite",
"value": "https://wordpress.ddev.site:8443/kontakt/",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "links"
}
},
"fonts": {
"gvid-d4a5bsz2gt": {
"id": "gvid-d4a5bsz2gt",
"label": "[Fixture] Akzentschrift",
"value": "Merriweather",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "fonts"
}
}
},
"stored_raw": {
"option_name": "et_divi_global_variables",
"autoload": "off",
"option_value": "a:5:{s:7:\"numbers\";a:2:{s:15:\"gvid-f6yq6mayfy\";a:7:{s:2:\"id\";s:15:\"gvid-f6yq6mayfy\";s:5:\"label\";s:21:\"[Fixture] Eckenradius\";s:5:\"value\";s:3:\"8px\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:7:\"numbers\";}s:15:\"gvid-5dec72fwsc\";a:7:{s:2:\"id\";s:15:\"gvid-5dec72fwsc\";s:5:\"label\";s:23:\"[Fixture] Abstand Karte\";s:5:\"value\";s:26:\"clamp(1.5rem, 4vw, 2.5rem)\";s:5:\"order\";s:1:\"2\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:7:\"numbers\";}}s:7:\"strings\";a:1:{s:15:\"gvid-7uckqqhmrh\";a:7:{s:2:\"id\";s:15:\"gvid-7uckqqhmrh\";s:5:\"label\";s:16:\"[Fixture] Slogan\";s:5:\"value\";s:38:\"Handwerk aus Meisterhand – seit 1987\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:7:\"strings\";}}s:6:\"images\";a:1:{s:15:\"gvid-qmc3cy1alq\";a:7:{s:2:\"id\";s:15:\"gvid-qmc3cy1alq\";s:5:\"label\";s:23:\"[Fixture] Werkstattbild\";s:5:\"value\";s:67:\"https://wordpress.ddev.site:8443/wp-content/uploads/2026/09/600.jpg\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:6:\"images\";}}s:5:\"links\";a:1:{s:15:\"gvid-nt8f6iz2qh\";a:7:{s:2:\"id\";s:15:\"gvid-nt8f6iz2qh\";s:5:\"label\";s:22:\"[Fixture] Kontaktseite\";s:5:\"value\";s:41:\"https://wordpress.ddev.site:8443/kontakt/\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:5:\"links\";}}s:5:\"fonts\";a:1:{s:15:\"gvid-d4a5bsz2gt\";a:7:{s:2:\"id\";s:15:\"gvid-d4a5bsz2gt\";s:5:\"label\";s:23:\"[Fixture] Akzentschrift\";s:5:\"value\";s:12:\"Merriweather\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:5:\"fonts\";}}}"
},
"get_global_variables": {
"numbers": {
"gvid-f6yq6mayfy": {
"id": "gvid-f6yq6mayfy",
"label": "[Fixture] Eckenradius",
"value": "8px",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "numbers",
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
},
"gvid-5dec72fwsc": {
"id": "gvid-5dec72fwsc",
"label": "[Fixture] Abstand Karte",
"value": "clamp(1.5rem, 4vw, 2.5rem)",
"order": "2",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "numbers",
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
}
},
"strings": {
"gvid-7uckqqhmrh": {
"id": "gvid-7uckqqhmrh",
"label": "[Fixture] Slogan",
"value": "Handwerk aus Meisterhand – seit 1987",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "strings",
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
}
},
"images": {
"gvid-qmc3cy1alq": {
"id": "gvid-qmc3cy1alq",
"label": "[Fixture] Werkstattbild",
"value": "https://wordpress.ddev.site:8443/wp-content/uploads/2026/09/600.jpg",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "images",
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
}
},
"links": {
"gvid-nt8f6iz2qh": {
"id": "gvid-nt8f6iz2qh",
"label": "[Fixture] Kontaktseite",
"value": "https://wordpress.ddev.site:8443/kontakt/",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "links",
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
}
},
"colors": {},
"fonts": {
"--et_global_heading_font": {
"id": "--et_global_heading_font",
"label": "Überschrift",
"value": "Open Sans",
"status": "active",
"order": 1,
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
},
"--et_global_body_font": {
"id": "--et_global_body_font",
"label": "Fließtext",
"value": "Open Sans",
"status": "active",
"order": 2,
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
},
"gvid-d4a5bsz2gt": {
"id": "gvid-d4a5bsz2gt",
"label": "[Fixture] Akzentschrift",
"value": "Merriweather",
"order": "1",
"status": "active",
"lastUpdated": "2026-09-27T22:21:39.000Z",
"variableType": "fonts",
"allowedActions": {
"editLabel": true,
"editValue": true,
"reorder": true,
"remove": true
}
}
}
},
"get_global_variables_php_types": {
"numbers": "stdClass",
"strings": "stdClass",
"images": "stdClass",
"links": "stdClass",
"colors": "stdClass",
"fonts": "stdClass"
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"divi_version": "5.2.1",
"builder_version": "5.2.1",
"files": [
"global-colors.json",
"global-variables.json",
"presets.json",
"presets-history.json",
"et-divi-subset.json",
"page--designsystem.json",
"page--designsystem.html"
],
"posts": {
"page--designsystem": 3182
}
}
@@ -0,0 +1,14 @@
<!-- wp:divi/placeholder --><!-- wp:divi/section {"module":{"meta":{"adminLabel":{"desktop":{"value":"Designsystem Abschnitt"}}}},"builderVersion":"5.2.1"} -->
<!-- wp:divi/row {"module":{"meta":{"adminLabel":{"desktop":{"value":"Designsystem Zeile"}}}},"builderVersion":"5.2.1"} -->
<!-- wp:divi/column {"module":{"decoration":{"sizing":{"desktop":{"value":{"flexType":"24_24"}}}}},"builderVersion":"5.2.1"} -->
<!-- wp:divi/heading {"module":{"meta":{"adminLabel":{"desktop":{"value":"Überschrift"}}}},"title":{"innerContent":{"desktop":{"value":"Designsystem-Fixture"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/text {"module":{"meta":{"adminLabel":{"desktop":{"value":"Text mit Modul-Preset"}}}},"modulePreset":["hsiy1sjxt7"],"content":{"innerContent":{"desktop":{"value":"\u003cp\u003eHinweis-Text mit dem Modul-Preset „[Fixture] Text Hinweis“ (enthält das Gruppen-Preset „[Fixture] Fließtext“).\u003c/p\u003e"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/text {"module":{"meta":{"adminLabel":{"desktop":{"value":"Text mit Gruppen-Preset"}}}},"groupPreset":{"content.decoration.bodyFont":{"presetId":["ja6o54jtlc"],"groupName":"divi/font-body"}},"content":{"innerContent":{"desktop":{"value":"\u003cp\u003eFließtext mit dem Gruppen-Preset „[Fixture] Fließtext“ direkt am Modul.\u003c/p\u003e"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/text {"module":{"meta":{"adminLabel":{"desktop":{"value":"Text mit String-Variable"}}}},"content":{"innerContent":{"desktop":{"value":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022gvid-7uckqqhmrh\u0022,\u0022settings\u0022:{}}})$"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/button {"module":{"meta":{"adminLabel":{"desktop":{"value":"Button mit Modul-Preset"}}}},"modulePreset":["3gbmxlurey"],"button":{"innerContent":{"desktop":{"value":{"text":"Angebot anfragen","linkUrl":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022gvid-nt8f6iz2qh\u0022,\u0022settings\u0022:{}}})$"}}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/blurb {"module":{"meta":{"adminLabel":{"desktop":{"value":"Blurb mit Modul- und Gruppen-Preset"}}}},"modulePreset":["vdcl6msnuo"],"groupPreset":{"module.decoration.spacing":{"presetId":["apzj7e17ok"],"groupName":"divi/spacing"}},"title":{"innerContent":{"desktop":{"value":"Möbel nach Maß"}}},"content":{"innerContent":{"desktop":{"value":"\u003cp\u003eBlurb mit „[Fixture] Blurb Leistung“ und dem Gruppen-Preset „[Fixture] Abstand Karte“.\u003c/p\u003e"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/image {"module":{"meta":{"adminLabel":{"desktop":{"value":"Bild aus Bild-Variable"}}}},"image":{"innerContent":{"desktop":{"value":{"src":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022gvid-qmc3cy1alq\u0022,\u0022settings\u0022:{}}})$","alt":"Werkstatt"}}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/divider {"module":{"meta":{"adminLabel":{"desktop":{"value":"Trenner ohne Preset (Standard-Preset)"}}}},"builderVersion":"5.2.1"} /-->
<!-- /wp:divi/column -->
<!-- /wp:divi/row -->
<!-- /wp:divi/section --><!-- /wp:divi/placeholder -->
@@ -0,0 +1,36 @@
{
"post": {
"ID": 3182,
"post_type": "page",
"post_status": "draft",
"post_title": "[Fixture] Designsystem",
"post_name": "",
"post_date_gmt": "0000-00-00 00:00:00",
"post_modified_gmt": "2026-09-27 22:21:39",
"content_file": "page--designsystem.html",
"content_sha256": "758485d1c95447ecae444e823a62eee1ce4d5e8525d13f607c0cdf4ab1ac207f"
},
"meta": {
"_et_pb_use_builder": [
"on"
],
"_et_pb_show_page_creation": [
"off"
],
"_et_pb_first_image": [
""
],
"_et_pb_truncate_post": [
""
],
"_et_pb_truncate_post_date": [
""
],
"_et_pb_use_divi_5": [
"on"
],
"et_enqueued_post_fonts": [
"a:3:{s:6:\"family\";a:2:{i:0;s:111:\"Merriweather:300,regular,500,600,700,800,900,300italic,italic,500italic,600italic,700italic,800italic,900italic\";i:1;s:94:\"Open+Sans:300,regular,500,600,700,800,300italic,italic,500italic,600italic,700italic,800italic\";}s:6:\"subset\";a:2:{i:0;s:5:\"latin\";i:1;s:9:\"latin-ext\";}s:9:\"cache_key\";s:74:\"{\"gph\":-1,\"divi\":\"5.2.1\",\"wp\":\"7.1.2\",\"enable_all_character_sets\":\"false\"}\";}"
]
}
}
@@ -0,0 +1,17 @@
{
"rows": [
{
"option_name": "et_divi_builder_presets_history_meta",
"autoload": "off",
"value": {
"index": -1,
"size": 0
}
}
],
"et_divi_flags": {
"presets_storage_migrated_flag": true,
"builder_global_presets_history_migrated": true,
"custom_defaults_migrated_flag": true
}
}
+909
View File
@@ -0,0 +1,909 @@
{
"option_name": "et_divi_builder_global_presets_d5",
"stored": {
"module": {
"divi/button": {
"default": "",
"items": {
"3gbmxlurey": {
"type": "module",
"id": "3gbmxlurey",
"name": "[Fixture] Button Primär",
"moduleName": "divi/button",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"button": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"font": {
"font": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$",
"weight": "600"
}
}
}
}
}
}
},
"styleAttrs": {
"button": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"font": {
"font": {
"desktop": {
"value": {
"weight": "600",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$"
}
}
}
}
}
}
}
}
}
},
"divi/text": {
"default": "",
"items": {
"hsiy1sjxt7": {
"type": "module",
"id": "hsiy1sjxt7",
"name": "[Fixture] Text Hinweis",
"moduleName": "divi/text",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"module": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"spacing": {
"desktop": {
"value": {
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
}
}
}
}
}
}
},
"styleAttrs": {
"module": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"spacing": {
"desktop": {
"value": {
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
}
}
}
}
}
}
},
"groupPresets": {
"content.decoration.bodyFont": {
"presetId": [
"ja6o54jtlc"
],
"groupName": "divi/font-body"
}
}
}
}
},
"divi/blurb": {
"default": "",
"items": {
"vdcl6msnuo": {
"type": "module",
"id": "vdcl6msnuo",
"name": "[Fixture] Blurb Leistung",
"moduleName": "divi/blurb",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"title": {
"decoration": {
"font": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$",
"weight": "700"
}
}
}
}
}
},
"module": {
"decoration": {
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
},
"styles": {
"all": {
"width": "2px",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$",
"style": "solid"
}
}
}
}
}
}
}
},
"styleAttrs": {
"module": {
"decoration": {
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
},
"styles": {
"all": {
"width": "2px",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$",
"style": "solid"
}
}
}
}
}
}
},
"title": {
"decoration": {
"font": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"weight": "700",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$"
}
}
}
}
}
}
}
}
}
},
"divi/divider": {
"default": "g7kvfq5uh8",
"items": {
"g7kvfq5uh8": {
"type": "module",
"id": "g7kvfq5uh8",
"name": "[Fixture] Trenner Standard",
"moduleName": "divi/divider",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"divider": {
"advanced": {
"line": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$",
"weight": "3px"
}
}
}
}
}
},
"styleAttrs": {
"divider": {
"advanced": {
"line": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$",
"weight": "3px"
}
}
}
}
}
}
}
}
}
},
"group": {
"divi/font-body": {
"default": "",
"items": {
"ja6o54jtlc": {
"type": "group",
"id": "ja6o54jtlc",
"name": "[Fixture] Fließtext",
"groupName": "divi/font-body",
"groupId": "content.decoration.bodyFont",
"moduleName": "divi/text",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"content": {
"decoration": {
"bodyFont": {
"body": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$",
"size": "17px",
"lineHeight": "1.8em"
}
}
}
}
}
}
}
},
"styleAttrs": {
"content": {
"decoration": {
"bodyFont": {
"body": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$",
"size": "17px",
"lineHeight": "1.8em"
}
}
}
}
}
}
}
}
}
}
},
"divi/spacing": {
"default": "",
"items": {
"apzj7e17ok": {
"type": "group",
"id": "apzj7e17ok",
"name": "[Fixture] Abstand Karte",
"groupName": "divi/spacing",
"groupId": "module.decoration.spacing",
"moduleName": "divi/blurb",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"module": {
"decoration": {
"spacing": {
"desktop": {
"value": {
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
},
"margin": {
"bottom": "24px"
}
}
}
}
}
}
},
"styleAttrs": {
"module": {
"decoration": {
"spacing": {
"desktop": {
"value": {
"margin": {
"bottom": "24px"
},
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
}
}
}
}
}
}
}
}
}
}
}
},
"get_data": {
"module": {
"divi/button": {
"default": "",
"items": {
"3gbmxlurey": {
"type": "module",
"id": "3gbmxlurey",
"name": "[Fixture] Button Primär",
"moduleName": "divi/button",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"button": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"font": {
"font": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$",
"weight": "600"
}
}
}
}
}
}
},
"styleAttrs": {
"button": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"font": {
"font": {
"desktop": {
"value": {
"weight": "600",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$"
}
}
}
}
}
}
}
}
}
},
"divi/text": {
"default": "",
"items": {
"hsiy1sjxt7": {
"type": "module",
"id": "hsiy1sjxt7",
"name": "[Fixture] Text Hinweis",
"moduleName": "divi/text",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"module": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"spacing": {
"desktop": {
"value": {
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
}
}
}
}
}
}
},
"styleAttrs": {
"module": {
"decoration": {
"background": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$"
}
}
},
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
}
}
}
},
"spacing": {
"desktop": {
"value": {
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
}
}
}
}
}
}
},
"groupPresets": {
"content.decoration.bodyFont": {
"presetId": [
"ja6o54jtlc"
],
"groupName": "divi/font-body"
}
}
}
}
},
"divi/blurb": {
"default": "",
"items": {
"vdcl6msnuo": {
"type": "module",
"id": "vdcl6msnuo",
"name": "[Fixture] Blurb Leistung",
"moduleName": "divi/blurb",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"title": {
"decoration": {
"font": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$",
"weight": "700"
}
}
}
}
}
},
"module": {
"decoration": {
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
},
"styles": {
"all": {
"width": "2px",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$",
"style": "solid"
}
}
}
}
}
}
}
},
"styleAttrs": {
"module": {
"decoration": {
"border": {
"desktop": {
"value": {
"radius": {
"sync": "on",
"topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$",
"bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$"
},
"styles": {
"all": {
"width": "2px",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$",
"style": "solid"
}
}
}
}
}
}
},
"title": {
"decoration": {
"font": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"weight": "700",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$"
}
}
}
}
}
}
}
}
}
},
"divi/divider": {
"default": "g7kvfq5uh8",
"items": {
"g7kvfq5uh8": {
"type": "module",
"id": "g7kvfq5uh8",
"name": "[Fixture] Trenner Standard",
"moduleName": "divi/divider",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"divider": {
"advanced": {
"line": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$",
"weight": "3px"
}
}
}
}
}
},
"styleAttrs": {
"divider": {
"advanced": {
"line": {
"desktop": {
"value": {
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$",
"weight": "3px"
}
}
}
}
}
}
}
}
}
},
"group": {
"divi/font-body": {
"default": "",
"items": {
"ja6o54jtlc": {
"type": "group",
"id": "ja6o54jtlc",
"name": "[Fixture] Fließtext",
"groupName": "divi/font-body",
"groupId": "content.decoration.bodyFont",
"moduleName": "divi/text",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"content": {
"decoration": {
"bodyFont": {
"body": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$",
"size": "17px",
"lineHeight": "1.8em"
}
}
}
}
}
}
}
},
"styleAttrs": {
"content": {
"decoration": {
"bodyFont": {
"body": {
"font": {
"desktop": {
"value": {
"family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$",
"color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$",
"size": "17px",
"lineHeight": "1.8em"
}
}
}
}
}
}
}
}
}
}
},
"divi/spacing": {
"default": "",
"items": {
"apzj7e17ok": {
"type": "group",
"id": "apzj7e17ok",
"name": "[Fixture] Abstand Karte",
"groupName": "divi/spacing",
"groupId": "module.decoration.spacing",
"moduleName": "divi/blurb",
"version": "5.2.1",
"created": 1790547699462,
"updated": 1790547699462,
"attrs": {
"module": {
"decoration": {
"spacing": {
"desktop": {
"value": {
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
},
"margin": {
"bottom": "24px"
}
}
}
}
}
}
},
"styleAttrs": {
"module": {
"decoration": {
"spacing": {
"desktop": {
"value": {
"margin": {
"bottom": "24px"
},
"padding": {
"top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$",
"syncVertical": "on",
"syncHorizontal": "on"
}
}
}
}
}
}
}
}
}
}
}
},
"legacy_d4_option": {
"option_name": "et_divi_builder_global_presets_ng",
"autoload": "off",
"option_value": "O:8:\"stdClass\":0:{}"
},
"is_legacy_presets_imported": ""
}
@@ -0,0 +1,191 @@
<?php
/**
* Exports the stage 4 design system fixtures from the local site into tests/Fixtures/divi/stage4/: global colors,
* global variables, presets (each as stored in the database and as Divi's getter returns it), the relevant et_divi
* subset, the (Divi 4) preset history options and the page "[Fixture] Designsystem" (post row, meta, post_content
* byte-exact). How the data was created: tests/Integration/divi-seed-stage4-design.php and
* tests/Fixtures/divi/stage4/README.md.
*
* Usage (DDEV, WordPress root):
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage4-fixtures.php
*
* Read-only. Fixtures are committed – no personal data. No `declare(strict_types=1);` – wp eval-file uses eval().
*/
defined('WP_CLI') && WP_CLI || exit;
use ET\Builder\Packages\GlobalData\GlobalData;
use ET\Builder\Packages\GlobalData\GlobalPreset;
$lit_dir = dirname(__DIR__) . '/Fixtures/divi/stage4';
$lit_page_title = '[Fixture] Designsystem';
$lit_write = static function (string $file, string $data): void {
if (file_put_contents($file, $data) === false) {
WP_CLI::error('Cannot write ' . $file);
}
};
$lit_json = static fn ($data): string => wp_json_encode(
$data,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
) . "\n";
/** The option row exactly as stored (serialized string, autoload flag) – shows the PHP types Divi writes. */
$lit_raw_option = static function (string $name): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s",
$name
));
return $row === null ? null : ['option_name' => $name, 'autoload' => $row->autoload, 'option_value' => $row->option_value];
};
$lit_pages = get_posts([
'post_type' => 'page',
'post_status' => ['draft', 'publish', 'private', 'pending'],
'title' => $lit_page_title,
'numberposts' => 1,
'orderby' => 'ID',
'order' => 'ASC',
]);
if ($lit_pages === []) {
WP_CLI::error('Page "' . $lit_page_title . '" not found – run tests/Integration/divi-seed-stage4-design.php first.');
}
$lit_page = $lit_pages[0];
if (! is_dir($lit_dir) && ! wp_mkdir_p($lit_dir)) {
WP_CLI::error('Cannot create ' . $lit_dir);
}
// Global colors: et_divi['et_global_data'] as stored, plus GlobalData::get_global_colors() (customizer colors merged
// in front; their lastUpdated is generated on every read).
$lit_et_divi = get_option('et_divi', []);
$lit_write($lit_dir . '/global-colors.json', $lit_json([
'stored' => [
'option_name' => 'et_divi',
'key' => 'et_global_data',
'value' => maybe_unserialize($lit_et_divi['et_global_data'] ?? null),
],
'get_global_colors' => GlobalData::get_global_colors(),
]));
// Global variables: product option et_divi_global_variables (arrays), getter result (every type is a stdClass –
// an empty type therefore encodes as {} here, not []).
$lit_write($lit_dir . '/global-variables.json', $lit_json([
'stored' => get_option('et_divi_global_variables', null),
'stored_raw' => $lit_raw_option('et_divi_global_variables'),
'get_global_variables' => GlobalData::get_global_variables(),
'get_global_variables_php_types' => array_map(
static fn ($items) => get_debug_type($items),
GlobalData::get_global_variables()
),
]));
// Presets: product option et_divi_builder_global_presets_d5, plus the D4 legacy option and import flag.
$lit_write($lit_dir . '/presets.json', $lit_json([
'option_name' => 'et_divi_' . GlobalPreset::option_name(),
'stored' => get_option('et_divi_' . GlobalPreset::option_name(), null),
'get_data' => GlobalPreset::get_data(),
'legacy_d4_option' => $lit_raw_option('et_divi_builder_global_presets_ng'),
'is_legacy_presets_imported' => GlobalPreset::is_legacy_presets_imported(),
]));
// Preset history (Divi 4 storage; Divi 5 preset saves do not write it).
global $wpdb;
$lit_history_rows = $wpdb->get_results(
"SELECT option_name, option_value, autoload FROM {$wpdb->options}
WHERE option_name LIKE 'et\\_divi\\_builder\\_presets\\_history%'
OR option_name LIKE 'et\\_divi\\_builder\\_global\\_presets\\_history%'
OR option_name LIKE 'et\\_divi\\_builder\\_custom\\_defaults\\_history%'
ORDER BY option_name"
);
$lit_write($lit_dir . '/presets-history.json', $lit_json([
'rows' => array_map(static fn ($row) => [
'option_name' => $row->option_name,
'autoload' => $row->autoload,
'value' => maybe_unserialize($row->option_value),
], $lit_history_rows),
'et_divi_flags' => array_intersect_key((array) $lit_et_divi, array_flip([
'builder_global_presets_history_migrated',
'presets_storage_migrated_flag',
'custom_defaults_migrated_flag',
])),
]));
// Theme options relevant to spec §7 (whitelist): present keys with their stored value, absent keys listed.
$lit_keys = [
'heading_font', 'body_font', 'heading_font_weight', 'body_font_weight', 'primary_nav_font', 'secondary_nav_font',
'all_buttons_font', 'body_font_size', 'tablet_body_font_size', 'phone_body_font_size', 'body_header_size',
'tablet_header_font_size', 'phone_header_font_size', 'body_font_height', 'body_header_height',
'body_header_spacing', 'body_header_style', 'content_width', 'gutter_width', 'section_padding', 'row_padding',
'accent_color', 'secondary_accent_color', 'header_color', 'font_color', 'link_color', 'divi_logo',
];
foreach (array_keys((array) $lit_et_divi) as $key) {
if (str_starts_with($key, 'all_buttons_') || str_starts_with($key, 'divi_seo_')) {
$lit_keys[] = $key;
}
}
$lit_keys = array_values(array_unique($lit_keys));
$lit_present = [];
$lit_absent = [];
foreach ($lit_keys as $key) {
if (array_key_exists($key, (array) $lit_et_divi)) {
$lit_present[$key] = $lit_et_divi[$key];
} else {
$lit_absent[] = $key;
}
}
$lit_write($lit_dir . '/et-divi-subset.json', $lit_json([
'option_name' => 'et_divi',
'present' => (object) $lit_present,
'absent' => $lit_absent,
'effective' => [
'accent_colors' => GlobalData::get_accent_color_values(),
'customizer_fonts' => GlobalData::get_customizer_fonts(),
],
]));
// The page: post row, all meta (raw), post_content byte-exact.
$lit_meta = [];
foreach ($wpdb->get_results($wpdb->prepare(
"SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d ORDER BY meta_id",
$lit_page->ID
)) as $row) {
if (! in_array($row->meta_key, ['_edit_lock', '_edit_last'], true)) {
$lit_meta[$row->meta_key][] = $row->meta_value;
}
}
$lit_write($lit_dir . '/page--designsystem.json', $lit_json([
'post' => [
'ID' => $lit_page->ID,
'post_type' => $lit_page->post_type,
'post_status' => $lit_page->post_status,
'post_title' => $lit_page->post_title,
'post_name' => $lit_page->post_name,
'post_date_gmt' => $lit_page->post_date_gmt,
'post_modified_gmt' => $lit_page->post_modified_gmt,
'content_file' => 'page--designsystem.html',
'content_sha256' => hash('sha256', $lit_page->post_content),
],
'meta' => (object) $lit_meta,
]));
$lit_write($lit_dir . '/page--designsystem.html', $lit_page->post_content);
$lit_write($lit_dir . '/manifest.json', $lit_json([
'divi_version' => wp_get_theme('Divi')->get('Version'),
'builder_version' => ET_BUILDER_VERSION,
'files' => [
'global-colors.json',
'global-variables.json',
'presets.json',
'presets-history.json',
'et-divi-subset.json',
'page--designsystem.json',
'page--designsystem.html',
],
'posts' => ['page--designsystem' => $lit_page->ID],
]));
WP_CLI::success('Stage 4 fixtures exported to ' . $lit_dir);
@@ -0,0 +1,558 @@
<?php
/**
* Seeds realistic Divi 5 design data for the Etappe 4 fixtures (spec §15 "Format der Presets/Variablen an echten
* Daten verifizieren"): four global colors (one relative to another), one global variable per type, four module
* presets (one of them the default of its module) and two option group presets that reference colors and variables,
* plus the draft page "[Fixture] Designsystem" that uses them.
*
* Everything is written through the REST endpoints the Visual Builder itself calls, dispatched in-process with
* rest_do_request() – so Divi's own argument schema, sanitize callbacks, permission callbacks and save functions run:
* POST divi/v1/global-data/global-colors → GlobalDataController::update_global_colors → GlobalData::set_global_colors
* POST divi/v1/global-data/global-variables → GlobalDataController::update_global_variables → GlobalData::set_global_variables
* POST divi/v1/global-data/global-preset/sync → GlobalPresetController::sync → GlobalPreset::prepare_data/save_data
* POST divi/v1/sync-to-server → SyncToServerController::update (like "Entwurf speichern")
* Formats and sources: docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md.
*
* Usage (DDEV, WordPress root):
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-seed-stage4-design.php
*
* Idempotent: entries are found again by their "[Fixture] …" label/name and updated in place (IDs, "created" and
* "lastUpdated" stay when nothing changed); nothing is duplicated. It never sends the five customizer colors
* (gcid-primary-color …) – their values in et_divi stay untouched – and never deletes anything.
* Runs as the first administrator when WP-CLI has no --user. No `declare(strict_types=1);` – wp eval-file uses eval().
*/
defined('WP_CLI') && WP_CLI || exit;
use ET\Builder\Framework\Controllers\RESTController;
use ET\Builder\Packages\Conversion\Conversion;
use ET\Builder\Packages\GlobalData\GlobalData;
use ET\Builder\Packages\GlobalData\GlobalPreset;
if (get_current_user_id() === 0) {
$lit_admins = get_users(['role' => 'administrator', 'number' => 1, 'orderby' => 'ID']);
if ($lit_admins === []) {
WP_CLI::error('No administrator found.');
}
wp_set_current_user($lit_admins[0]->ID);
}
if (! current_user_can('edit_theme_options') || ! et_pb_is_allowed('variables_manager')) {
WP_CLI::error('The current user needs edit_theme_options and Divi\'s role right "variables_manager".');
}
/** Random ID like the Visual Builder's generateNanoUuid(): 10 characters of [0-9a-z]. */
$lit_nano = static function (): string {
$alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
$id = '';
for ($i = 0; $i < 10; $i++) {
$id .= $alphabet[random_int(0, 35)];
}
return $id;
};
/**
* Dispatches a request to one of Divi's REST routes as the current user; stops on any error. Divi checks a per-route
* nonce in the X-ET-Nonce header (RESTRoute, action "divi/v1<route>--POST"), created here like the builder gets it.
*/
$lit_rest = static function (string $route, array $body): array {
$request = new WP_REST_Request('POST', '/divi/v1' . $route);
$request->set_header('Content-Type', 'application/json');
$request->set_header('X-ET-Nonce', RESTController::create_nonce('divi/v1', $route, 'POST'));
$request->set_body(wp_json_encode($body));
$response = rest_do_request($request);
$data = rest_get_server()->response_to_data($response, false);
if ($response->is_error() || $response->get_status() >= 300) {
WP_CLI::error(sprintf('%s → HTTP %d: %s', $route, $response->get_status(), wp_json_encode($data)));
}
return is_array($data) ? $data : (array) $data;
};
/** Dynamic reference to a global color, optionally with HSL/opacity adjustments (relative color). */
$lit_color_ref = static fn (string $id, array $settings = []): string => '$variable(' . wp_json_encode(
['type' => 'color', 'value' => ['name' => $id, 'settings' => (object) $settings]]
) . ')$';
/** Dynamic reference to a global variable (numbers, strings, images, links, fonts). */
$lit_var_ref = static fn (string $id): string => '$variable(' . wp_json_encode(
['type' => 'content', 'value' => ['name' => $id, 'settings' => (object) []]]
) . ')$';
$lit_now_iso = gmdate('Y-m-d\TH:i:s.v\Z');
$lit_now_ms = (int) round(microtime(true) * 1000);
// ---------------------------------------------------------------------------------------------------------------
// 1. Global colors
// ---------------------------------------------------------------------------------------------------------------
$lit_colors_before = GlobalData::get_global_colors();
$lit_color_id = static function (string $label) use ($lit_colors_before, $lit_nano): string {
foreach ($lit_colors_before as $id => $color) {
if (is_array($color) && ($color['label'] ?? '') === $label && ! isset(GlobalData::$customizer_colors[$id])) {
return (string) $id;
}
}
return 'gcid-' . $lit_nano();
};
$lit_c = [
'holz_dunkel' => $lit_color_id('[Fixture] Holz dunkel'),
'holz_hell' => $lit_color_id('[Fixture] Holz hell'),
'salbei' => $lit_color_id('[Fixture] Salbeigrün'),
'sand' => $lit_color_id('[Fixture] Sandbeige'),
];
$lit_color_specs = [
$lit_c['holz_dunkel'] => ['[Fixture] Holz dunkel', '#5b3a29'],
// Relative color: Holz dunkel, 30 % lighter – stored as a $variable() reference with HSL settings.
$lit_c['holz_hell'] => ['[Fixture] Holz hell', $lit_color_ref($lit_c['holz_dunkel'], ['lightness' => 30])],
$lit_c['salbei'] => ['[Fixture] Salbeigrün', '#7d9b76'],
$lit_c['sand'] => ['[Fixture] Sandbeige', '#efe6d8'],
];
$lit_colors_payload = [];
foreach ($lit_color_specs as $id => [$label, $value]) {
$old = $lit_colors_before[$id] ?? null;
$unchanged = is_array($old) && ($old['color'] ?? null) === $value && ($old['status'] ?? '') === 'active';
$lit_colors_payload[$id] = [
'color' => $value,
'folder' => '',
'label' => $label,
'lastUpdated' => $unchanged ? (string) $old['lastUpdated'] : $lit_now_iso,
'status' => 'active',
'usedInPosts' => is_array($old['usedInPosts'] ?? null) ? $old['usedInPosts'] : [],
];
}
// Only the fixture colors are sent: the endpoint merges (set_global_colors(…, true)), and leaving out the customizer
// colors keeps accent_color & co. in et_divi untouched.
$lit_rest('/global-data/global-colors', ['global_colors' => $lit_colors_payload]);
WP_CLI::log('Colors: ' . implode(', ', array_map(
static fn ($id, $spec) => $spec[0] . ' = ' . $id,
array_keys($lit_color_specs),
$lit_color_specs
)));
// ---------------------------------------------------------------------------------------------------------------
// 2. Global variables – the endpoint REPLACES the whole option, so the full current set is sent.
// ---------------------------------------------------------------------------------------------------------------
$lit_vars = [];
foreach (GlobalData::get_global_variables() as $type => $items) {
if (! in_array($type, ['numbers', 'strings', 'images', 'links', 'fonts'], true)) {
continue; // "colors" is never stored in global_variables (sanitize_global_variables_data drops it).
}
foreach ((array) $items as $id => $item) {
if ($type === 'fonts' && isset(GlobalData::$customizer_fonts[$id])) {
continue; // Customizer fonts come from et_divi heading_font/body_font, not from this option.
}
unset($item['allowedActions']);
$lit_vars[$type][$id] = $item;
}
}
$lit_var_id = static function (string $type, string $label) use (&$lit_vars, $lit_nano): string {
foreach ($lit_vars[$type] ?? [] as $id => $item) {
if (($item['label'] ?? '') === $label) {
return (string) $id;
}
}
return 'gvid-' . $lit_nano();
};
$lit_image = get_posts(['post_type' => 'attachment', 'post_mime_type' => 'image', 'numberposts' => 1, 'orderby' => 'ID', 'order' => 'ASC']);
$lit_image_url = $lit_image !== [] ? (string) wp_get_attachment_url($lit_image[0]->ID) : get_template_directory_uri() . '/images/logo.png';
$lit_var_specs = [
'numbers' => [
'eckenradius' => ['[Fixture] Eckenradius', '8px'],
'abstand' => ['[Fixture] Abstand Karte', 'clamp(1.5rem, 4vw, 2.5rem)'],
],
'strings' => ['slogan' => ['[Fixture] Slogan', 'Handwerk aus Meisterhand – seit 1987']],
'images' => ['werkstatt' => ['[Fixture] Werkstattbild', $lit_image_url]],
'links' => ['kontakt' => ['[Fixture] Kontaktseite', home_url('/kontakt/')]],
'fonts' => ['akzent' => ['[Fixture] Akzentschrift', 'Merriweather']],
];
$lit_v = [];
foreach ($lit_var_specs as $type => $specs) {
foreach ($specs as $key => [$label, $value]) {
$id = $lit_var_id($type, $label);
$lit_v[$key] = $id;
$old = $lit_vars[$type][$id] ?? null;
$unchanged = is_array($old) && (string) ($old['value'] ?? '') === $value && ($old['status'] ?? '') === 'active';
$order = is_array($old) && isset($old['order'])
? (int) $old['order']
: 1 + count(array_filter($lit_vars[$type] ?? [], static fn ($i) => ($i['status'] ?? '') === 'active'));
$lit_vars[$type][$id] = [
'id' => $id,
'label' => $label,
'value' => $value,
'order' => $order,
'status' => 'active',
'lastUpdated' => $unchanged && isset($old['lastUpdated']) ? (string) $old['lastUpdated'] : $lit_now_iso,
'variableType' => $type,
];
}
}
$lit_rest('/global-data/global-variables', ['global_variables' => $lit_vars]);
WP_CLI::log('Variables: ' . wp_json_encode($lit_v));
// ---------------------------------------------------------------------------------------------------------------
// 3. Presets – the sync endpoint REPLACES the whole option; it refuses a payload with fewer presets than stored.
// ---------------------------------------------------------------------------------------------------------------
$lit_presets = GlobalPreset::get_data();
$lit_find_preset = static function (string $kind, string $sub, string $name) use ($lit_presets): ?array {
foreach ($lit_presets[$kind][$sub]['items'] ?? [] as $item) {
if (is_array($item) && ($item['name'] ?? '') === $name) {
return $item;
}
}
return null;
};
/** Splits preset attrs into styleAttrs / renderAttrs with Divi's own attribute map (Conversion, D4→D5 preset path). */
$lit_split = static function (string $moduleName, array $attrs): array {
$map = Conversion::get_preset_attrs_mapping($moduleName);
return [
Conversion::get_preset_attrs($attrs, ['style'], $map),
Conversion::get_preset_attrs($attrs, ['html', 'script'], $map),
];
};
/** Builds one preset item like the Visual Builder's generateNewPreset / generateNewOptionGroupPreset. */
$lit_preset = static function (string $kind, string $sub, array $spec) use ($lit_find_preset, $lit_split, $lit_nano, $lit_now_ms): array {
$old = $lit_find_preset($kind, $sub, $spec['name']);
[$styleAttrs, $renderAttrs] = $lit_split($spec['moduleName'], $spec['attrs']);
$item = ['type' => $kind, 'id' => $old['id'] ?? $lit_nano(), 'name' => $spec['name']];
if ($kind === 'group') {
$item += ['groupName' => $sub, 'groupId' => $spec['groupId']];
}
$item += [
'moduleName' => $spec['moduleName'],
'version' => ET_BUILDER_VERSION,
'created' => (int) ($old['created'] ?? $lit_now_ms),
'updated' => (int) ($old['updated'] ?? $lit_now_ms),
'attrs' => $spec['attrs'],
];
if ($styleAttrs !== []) {
$item['styleAttrs'] = $styleAttrs;
}
if ($renderAttrs !== []) {
$item['renderAttrs'] = $renderAttrs;
}
if (! empty($spec['groupPresets'])) {
$item['groupPresets'] = $spec['groupPresets'];
}
if ($old !== null) {
$compare = static function (array $preset): string {
unset($preset['updated'], $preset['priority'], $preset['order']);
ksort($preset);
return wp_json_encode($preset);
};
if ($compare($item) !== $compare($old)) {
$item['updated'] = $lit_now_ms;
}
foreach (['priority', 'order'] as $key) {
if (isset($old[$key])) {
$item[$key] = $old[$key];
}
}
}
return $item;
};
$lit_radius = static fn (string $value): array => [
'sync' => 'on',
'topLeft' => $value,
'topRight' => $value,
'bottomLeft' => $value,
'bottomRight' => $value,
];
$lit_padding = static fn (string $value): array => [
'top' => $value,
'right' => $value,
'bottom' => $value,
'left' => $value,
'syncVertical' => 'on',
'syncHorizontal' => 'on',
];
// Option group presets first – the text module preset nests the body-font preset.
$lit_group_font = $lit_preset('group', 'divi/font-body', [
'name' => '[Fixture] Fließtext',
'moduleName' => 'divi/text',
'groupId' => 'content.decoration.bodyFont',
'attrs' => ['content' => ['decoration' => ['bodyFont' => ['body' => ['font' => ['desktop' => ['value' => [
'family' => $lit_var_ref($lit_v['akzent']),
'color' => $lit_color_ref($lit_c['holz_dunkel']),
'size' => '17px',
'lineHeight' => '1.8em',
]]]]]]]],
]);
$lit_group_spacing = $lit_preset('group', 'divi/spacing', [
'name' => '[Fixture] Abstand Karte',
'moduleName' => 'divi/blurb',
'groupId' => 'module.decoration.spacing',
'attrs' => ['module' => ['decoration' => ['spacing' => ['desktop' => ['value' => [
'padding' => $lit_padding($lit_var_ref($lit_v['abstand'])),
'margin' => ['bottom' => '24px'],
]]]]]],
]);
$lit_module_button = $lit_preset('module', 'divi/button', [
'name' => '[Fixture] Button Primär',
'moduleName' => 'divi/button',
'attrs' => ['button' => ['decoration' => [
'background' => ['desktop' => ['value' => ['color' => $lit_color_ref($lit_c['holz_dunkel'])]]],
'border' => ['desktop' => ['value' => ['radius' => $lit_radius($lit_var_ref($lit_v['eckenradius']))]]],
'font' => ['font' => ['desktop' => ['value' => [
'color' => $lit_color_ref($lit_c['sand']),
'weight' => '600',
]]]],
]]],
]);
$lit_module_text = $lit_preset('module', 'divi/text', [
'name' => '[Fixture] Text Hinweis',
'moduleName' => 'divi/text',
'attrs' => ['module' => ['decoration' => [
'background' => ['desktop' => ['value' => ['color' => $lit_color_ref($lit_c['sand'])]]],
'border' => ['desktop' => ['value' => ['radius' => $lit_radius($lit_var_ref($lit_v['eckenradius']))]]],
'spacing' => ['desktop' => ['value' => ['padding' => $lit_padding($lit_var_ref($lit_v['abstand']))]]],
]]],
'groupPresets' => [
'content.decoration.bodyFont' => ['presetId' => [$lit_group_font['id']], 'groupName' => 'divi/font-body'],
],
]);
$lit_module_blurb = $lit_preset('module', 'divi/blurb', [
'name' => '[Fixture] Blurb Leistung',
'moduleName' => 'divi/blurb',
'attrs' => [
'title' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => [
'family' => $lit_var_ref($lit_v['akzent']),
'color' => $lit_color_ref($lit_c['holz_dunkel']),
'weight' => '700',
]]]]]],
'module' => ['decoration' => ['border' => ['desktop' => ['value' => [
'radius' => $lit_radius($lit_var_ref($lit_v['eckenradius'])),
'styles' => ['all' => ['width' => '2px', 'color' => $lit_color_ref($lit_c['salbei']), 'style' => 'solid']],
]]]]],
],
]);
// Default preset of divi/divider – a module no other page of this site uses, so the default changes nothing else.
$lit_module_divider = $lit_preset('module', 'divi/divider', [
'name' => '[Fixture] Trenner Standard',
'moduleName' => 'divi/divider',
'attrs' => ['divider' => ['advanced' => ['line' => ['desktop' => ['value' => [
'color' => $lit_color_ref($lit_c['holz_hell']),
'weight' => '3px',
]]]]]],
]);
$lit_mine = [
'module' => [
'divi/button' => [$lit_module_button],
'divi/text' => [$lit_module_text],
'divi/blurb' => [$lit_module_blurb],
'divi/divider' => [$lit_module_divider],
],
'group' => [
'divi/font-body' => [$lit_group_font],
'divi/spacing' => [$lit_group_spacing],
],
];
$lit_defaults = ['module' => ['divi/divider' => $lit_module_divider['id']], 'group' => []];
// Stored format (option) → REST schema format (list of {default, items: list}), merged with our items.
$lit_payload = ['module' => [], 'group' => []];
foreach (['module', 'group'] as $kind) {
$subs = array_unique(array_merge(array_keys($lit_presets[$kind] ?? []), array_keys($lit_mine[$kind])));
foreach ($subs as $sub) {
$items = $lit_presets[$kind][$sub]['items'] ?? [];
foreach ($lit_mine[$kind][$sub] ?? [] as $item) {
$items[$item['id']] = $item;
}
$lit_payload[$kind][] = [
'default' => (string) ($lit_defaults[$kind][$sub] ?? ($lit_presets[$kind][$sub]['default'] ?? '')),
'items' => array_values($items),
];
}
}
$lit_rest('/global-data/global-preset/sync', ['presets' => $lit_payload, 'actionType' => 'ADD_NEW_MODULE_PRESET']);
WP_CLI::log('Presets: ' . wp_json_encode([
'button' => $lit_module_button['id'],
'text' => $lit_module_text['id'],
'blurb' => $lit_module_blurb['id'],
'divider (default)' => $lit_module_divider['id'],
'font-body' => $lit_group_font['id'],
'spacing' => $lit_group_spacing['id'],
]));
// ---------------------------------------------------------------------------------------------------------------
// 4. Page "[Fixture] Designsystem" (draft) with preset and variable references, saved like the builder's draft save.
// ---------------------------------------------------------------------------------------------------------------
$lit_block = static function (string $name, array $attrs, array $inner = []): string {
$attrs['builderVersion'] = ET_BUILDER_VERSION;
$json = serialize_block_attributes($attrs);
if ($inner === []) {
return "<!-- wp:{$name} {$json} /-->\n";
}
return "<!-- wp:{$name} {$json} -->\n" . implode('', $inner) . "<!-- /wp:{$name} -->\n";
};
$lit_label = static fn (string $label): array => ['meta' => ['adminLabel' => ['desktop' => ['value' => $label]]]];
$lit_text = static fn (string $html): array => ['innerContent' => ['desktop' => ['value' => $html]]];
$lit_modules = [
$lit_block('divi/heading', [
'module' => $lit_label('Überschrift'),
'title' => ['innerContent' => ['desktop' => ['value' => 'Designsystem-Fixture']]],
]),
$lit_block('divi/text', [
'module' => $lit_label('Text mit Modul-Preset'),
'modulePreset' => [$lit_module_text['id']],
'content' => $lit_text('<p>Hinweis-Text mit dem Modul-Preset „[Fixture] Text Hinweis“ (enthält das Gruppen-Preset „[Fixture] Fließtext“).</p>'),
]),
$lit_block('divi/text', [
'module' => $lit_label('Text mit Gruppen-Preset'),
'groupPreset' => ['content.decoration.bodyFont' => ['presetId' => [$lit_group_font['id']], 'groupName' => 'divi/font-body']],
'content' => $lit_text('<p>Fließtext mit dem Gruppen-Preset „[Fixture] Fließtext“ direkt am Modul.</p>'),
]),
$lit_block('divi/text', [
'module' => $lit_label('Text mit String-Variable'),
'content' => $lit_text($lit_var_ref($lit_v['slogan'])),
]),
$lit_block('divi/button', [
'module' => $lit_label('Button mit Modul-Preset'),
'modulePreset' => [$lit_module_button['id']],
'button' => ['innerContent' => ['desktop' => ['value' => [
'text' => 'Angebot anfragen',
'linkUrl' => $lit_var_ref($lit_v['kontakt']),
]]]],
]),
$lit_block('divi/blurb', [
'module' => $lit_label('Blurb mit Modul- und Gruppen-Preset'),
'modulePreset' => [$lit_module_blurb['id']],
'groupPreset' => ['module.decoration.spacing' => ['presetId' => [$lit_group_spacing['id']], 'groupName' => 'divi/spacing']],
'title' => $lit_text('Möbel nach Maß'),
'content' => $lit_text('<p>Blurb mit „[Fixture] Blurb Leistung“ und dem Gruppen-Preset „[Fixture] Abstand Karte“.</p>'),
]),
$lit_block('divi/image', [
'module' => $lit_label('Bild aus Bild-Variable'),
'image' => ['innerContent' => ['desktop' => ['value' => [
'src' => $lit_var_ref($lit_v['werkstatt']),
'alt' => 'Werkstatt',
]]]],
]),
$lit_block('divi/divider', [
'module' => $lit_label('Trenner ohne Preset (Standard-Preset)'),
]),
];
$lit_column = $lit_block('divi/column', [
'module' => ['decoration' => ['sizing' => ['desktop' => ['value' => ['flexType' => '24_24']]]]],
], $lit_modules);
$lit_row = $lit_block('divi/row', ['module' => $lit_label('Designsystem Zeile')], [$lit_column]);
$lit_section = $lit_block('divi/section', ['module' => $lit_label('Designsystem Abschnitt')], [$lit_row]);
$lit_content = '<!-- wp:divi/placeholder -->' . rtrim($lit_section, "\n") . '<!-- /wp:divi/placeholder -->';
$lit_page_title = '[Fixture] Designsystem';
$lit_existing = get_posts([
'post_type' => 'page',
'post_status' => ['draft', 'publish', 'private', 'pending'],
'title' => $lit_page_title,
'numberposts' => 1,
'orderby' => 'ID',
'order' => 'ASC',
]);
if ($lit_existing !== []) {
$lit_page_id = $lit_existing[0]->ID;
} else {
$lit_page_id = wp_insert_post([
'post_type' => 'page',
'post_status' => 'draft',
'post_title' => $lit_page_title,
'post_content' => '',
], true);
if (is_wp_error($lit_page_id)) {
WP_CLI::error($lit_page_id->get_error_message());
}
et_builder_enable_for_post($lit_page_id);
}
if (get_post_field('post_content', $lit_page_id) !== $lit_content) {
$lit_rest('/sync-to-server', [
'post_id' => $lit_page_id,
'post_status' => get_post_status($lit_page_id),
'syncType' => 'draft',
'content' => ['post_content' => $lit_content],
]);
}
WP_CLI::log('Page: ' . $lit_page_id);
// ---------------------------------------------------------------------------------------------------------------
// 5. Read back through Divi and through the plugin's DesignSystemReader.
// ---------------------------------------------------------------------------------------------------------------
$lit_colors_after = GlobalData::get_global_colors();
foreach ($lit_color_specs as $id => [$label, $value]) {
if (($lit_colors_after[$id]['color'] ?? null) !== $value || ($lit_colors_after[$id]['label'] ?? null) !== $label) {
WP_CLI::error("Color {$id} did not read back.");
}
}
$lit_vars_after = GlobalData::get_global_variables();
foreach ($lit_var_specs as $type => $specs) {
foreach ($specs as $key => [$label, $value]) {
$item = ((array) ($lit_vars_after[$type] ?? []))[$lit_v[$key]] ?? null;
if (! is_array($item) || $item['value'] !== $value || $item['label'] !== $label) {
WP_CLI::error("Variable {$lit_v[$key]} ({$type}) did not read back.");
}
}
}
$lit_presets_after = GlobalPreset::get_data();
foreach ($lit_mine as $kind => $subs) {
foreach ($subs as $sub => $items) {
foreach ($items as $item) {
if (($lit_presets_after[$kind][$sub]['items'][$item['id']]['name'] ?? null) !== $item['name']) {
WP_CLI::error("Preset {$item['id']} ({$kind} {$sub}) did not read back.");
}
}
}
}
if (($lit_presets_after['module']['divi/divider']['default'] ?? '') !== $lit_module_divider['id']) {
WP_CLI::error('The divider default preset did not read back.');
}
if (class_exists(\LIT\MCP\Integrations\Divi\Design\DesignSystemReader::class)) {
$lit_ds = (new \LIT\MCP\Integrations\Divi\Design\DesignSystemReader())->read();
$lit_labels = array_column($lit_ds->colors(), 'label');
$lit_var_labels = array_column($lit_ds->variables(), 'label');
$lit_preset_names = array_values($lit_ds->presetNames());
foreach (array_column($lit_color_specs, 0) as $label) {
in_array($label, $lit_labels, true) || WP_CLI::error("DesignSystemReader misses color {$label}.");
}
foreach ($lit_var_specs as $specs) {
foreach ($specs as [$label]) {
in_array($label, $lit_var_labels, true) || WP_CLI::error("DesignSystemReader misses variable {$label}.");
}
}
foreach ($lit_mine as $subs) {
foreach ($subs as $items) {
foreach ($items as $item) {
in_array($item['name'], $lit_preset_names, true) || WP_CLI::error("DesignSystemReader misses preset {$item['name']}.");
}
}
}
WP_CLI::log(sprintf(
'DesignSystemReader: %d colors, %d variables, %d presets, fonts %s',
count($lit_ds->colors()),
count($lit_ds->variables()),
count($lit_ds->presets()),
wp_json_encode($lit_ds->fonts())
));
} else {
WP_CLI::warning('DesignSystemReader not loaded (plugin inactive?) – skipped.');
}
WP_CLI::success('Stage 4 design data seeded; page ' . $lit_page_id . '.');