diff --git a/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md b/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md index fbb1dc4..12918a9 100644 --- a/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md +++ b/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md @@ -18,6 +18,16 @@ Geprüft gegen Divi 5.15.1 (installiert, freigegeben): - Das Mapping (`Conversion::get_preset_attrs_mapping("divi/button")`) liefert `attrName`, `preset` und bei Bedarf `subName`. `preset` ist nicht immer eine Liste: gesehen wurden `"meta"`, `"content"` (String) und `["html"]` (Array). - Keine weiteren Abweichungen zu §3, außer den oben genannten. +## Nachtrag Divi 5.15.1 – Farben und Variablen (Etappe 4c, 2026-10-19) + +Geprüft gegen den installierten Quelltext `includes/builder-5/server/Packages/GlobalData/GlobalData.php` (5.15.1): + +- **Farben unverändert:** `set_global_colors(array $data, $already_sanitized = false): void` mischt mit `true` (`array_merge`), ersetzt mit `false` und bereinigt dann selbst; die fünf Hauptfarben gehen über `et_update_option()` in `et_divi` und werden aus den Daten entfernt; danach `remove_static_resources('all', 'all', true)`. `sanitize_global_colors_data(array $data): array` ist öffentlich und verlangt, dass `usedInPosts` ein Array ist. +- **`get_customizer_colors()` hat einen `static`-Cache ohne Reset.** Nach einer Änderung der Hauptfarbe liefert `get_global_colors()` im selben Request noch den alten Wert (`$_cached_data` wird zurückgesetzt, der `static` nicht). LIT MCP liest die fünf Hauptfarben deshalb für das Zurücklesen aus `et_divi` (`GlobalColorsAdapter::read()`). +- **Variablen: neue Signatur und Prüfungen.** `set_global_variables(array $data, ?array $expected_variables = null): ?WP_Error` kehrt weiterhin still zurück, wenn `edit_theme_options` oder `variables_manager` fehlt; sonst läuft es unter einer MySQL-Sperre (`GET_LOCK`), prüft optional den erwarteten Bestand (`global_variables_stale`, 409), verlangt, dass **jede gespeicherte `gvid-`-ID im neuen Bestand vorkommt** (`validate_global_variable_ids()` → `global_variable_ids_missing`, 400) und prüft geänderte Einträge (`validate_global_variable_changes()`: `status` `active`|`archived`, aktive Variablen mit nicht leerem `value`). **Folge: Variablen lassen sich über Divis Funktionen nicht löschen, nur archivieren** (Ruling V1 des Plans 4c). +- **Neuer Typ `gradients`** (`GLOBAL_VARIABLE_TYPES = numbers, strings, images, links, fonts, gradients`); `strings` und `gradients` behalten ihren Wert zeichengetreu. `get_global_variables()` liefert zusätzlich die leere Gruppe `colors`, wandelt Objekt-Gruppen in Arrays und fügt `allowedActions` und die Customizer-Schriften hinzu (jede Gruppe als `stdClass`); beim Zurückschreiben bleiben nur Speicherformat-Felder. +- **Archivierte Variablen:** `_sort_global_variable_snapshot()` behandelt `order` archivierter Variablen als `''`; der Builder setzt beim Archivieren `order: null`, das Sanitize macht daraus `''`. + ## 0. Grundlagen: wo Divi speichert `et_get_option()`/`et_update_option()` (`epanel/custom_functions.php:211`, `:272`) haben drei Ziele: diff --git a/docs/superpowers/notes/2026-10-18-divi-faehigkeiten.md b/docs/superpowers/notes/2026-10-18-divi-faehigkeiten.md index ef34c5e..af15f58 100644 --- a/docs/superpowers/notes/2026-10-18-divi-faehigkeiten.md +++ b/docs/superpowers/notes/2026-10-18-divi-faehigkeiten.md @@ -91,4 +91,4 @@ beeinflussen, ohne dass ein Test ihn bräuchte. - Der Admin-Hinweis berechnet den Zustand auf jeder Admin-Seite (Reflection und Dateihashes je Aufruf); bei Bedarf kurzlebig cachen. -- `DesignArea::feature()` braucht für Farben, Variablen und Theme-Optionen später eine eigene Fähigkeit (Etappe 4c/4d). +- `DesignArea::feature()`: Farben (`color-write`) und Variablen (`variable-write`) haben seit Etappe 4c eigene Fähigkeiten; Theme-Optionen bekommen ihre mit 4d. diff --git a/docs/superpowers/specs/2026-10-16-etappe-4-designsystem-design.md b/docs/superpowers/specs/2026-10-16-etappe-4-designsystem-design.md index 3c68db9..ea3ba81 100644 --- a/docs/superpowers/specs/2026-10-16-etappe-4-designsystem-design.md +++ b/docs/superpowers/specs/2026-10-16-etappe-4-designsystem-design.md @@ -169,10 +169,10 @@ Jedes Tool nimmt **mehrere Einträge** eines Bereichs je Aufruf (ein Snapshot, e ### 7.2 `lit/divi-variables-set` -- `items`: `{ id? | name?, "type": "numbers|strings|images|links|fonts", "label", "value", "status": "active"|"archived" }`, `delete: true` für echtes Löschen. +- `items`: `{ id? | name?, "type": "numbers|strings|images|links|fonts", "label", "value", "status": "active"|"archived" }`. Echtes Löschen gibt es nicht: Divi 5.15.1 verweigert jeden Bestand, dem eine gespeicherte `gvid-`-ID fehlt (Plan 4c, Ruling V1). - Prüfung je Typ (Zahlen: CSS-Ausdruck ohne `;{}`, Links: URL, Bilder: Anhang-ID/URL der Mediathek, Schriften: §8.2, Strings: Text). - Schreiben: vollständig lesen, `stdClass` → Array, `allowedActions` und Customizer-Schriften entfernen, `set_global_variables()` (ersetzt), zurücklesen (stiller Fehlschlag bei fehlendem `variables_manager`). -- Löschen nur unbenutzt (G5; Fundstellen auch in Presets und relativen Farben), sonst `in_use` mit Hinweis auf detach. „Archivieren“ geht immer (Divi-Verhalten). +- Archivieren geht immer (Divi-Verhalten); der Probelauf nennt Fundstellen in Seiten, Presets und Farben als Warnung, `lit/divi-design-detach` löst die Verweise vorher (§6.8). Ein Restore behält später angelegte Variablen archiviert. ## 8. Theme-Optionen und Schriften diff --git a/tests/Integration/divi-colors-variables-smoke.sh b/tests/Integration/divi-colors-variables-smoke.sh new file mode 100755 index 0000000..7ef4ac8 --- /dev/null +++ b/tests/Integration/divi-colors-variables-smoke.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# End-to-end checks of Etappe 4c (global colors and variables: create, relative color, cycle, main color, archive, +# detach on a page, snapshots, restore) through the OAuth-protected endpoint against the real Divi. Runs INSIDE the +# DDEV web container: +# cd /home/janniclas/Projekte/WordPress && timeout 900 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-colors-variables-smoke.sh +# Creates colors/variables named "LIT Smoke …" and one draft page. The EXIT trap puts the options et_divi (colors, +# customizer colors) and et_divi_global_variables back to what they were and deletes the page by exact ID. +set -euo pipefail +cd /var/www/html +# shellcheck source=lib.sh +source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh + +ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1) +RUN="$(date +%s)-$$" +PREFIX="LIT Smoke $RUN" +ORIGINAL_THEME=$(wp option get et_divi --format=json 2>/dev/null || echo '{}') +ORIGINAL_VARIABLES=$(wp option get et_divi_global_variables --format=json 2>/dev/null || echo '{}') +PAGE_ID="" + +cleanup() { + printf '%s' "$LIT_THEME" | wp option update et_divi --format=json >/dev/null 2>&1 \ + || echo "WARN: et_divi not restored" + printf '%s' "$LIT_VARIABLES" | wp option update et_divi_global_variables --format=json >/dev/null 2>&1 \ + || echo "WARN: global variables not restored" + wp eval 'ET_Core_PageResource::remove_static_resources("all", "all", true);' >/dev/null 2>&1 || true + if [ -n "$PAGE_ID" ]; then wp post delete "$PAGE_ID" --force >/dev/null 2>&1 || true; fi + cleanup_app_passwords_for "$ADMIN_LOGIN" +} +export LIT_THEME="$ORIGINAL_THEME" LIT_VARIABLES="$ORIGINAL_VARIABLES" +trap 'rc=$?; cleanup; guard_exit "$rc" || exit $?' EXIT + +connect "$ADMIN_LOGIN" + +# confirm : dry run must answer confirm_required; then the confirmed call; prints the result. +confirm() { + local dry rev + dry=$(ability "$1" "$2") + [ "$(error_code "$dry")" = "confirm_required" ] || fail "$1 dry run: $dry" + rev=$(echo "$dry" | jq -r '.__error | fromjson | .design_rev') + ability "$1" "$(echo "$2" | jq -c --arg r "$rev" '. + {confirm: true, design_rev: $r}')" +} +stored_color() { # $1 = label -> the stored color value + wp eval "foreach (ET\Builder\Packages\GlobalData\GlobalData::get_global_colors() as \$c) { if ((\$c['label'] ?? '') === '$1') { echo \$c['color']; } }" +} +color_id() { # $1 = label + wp eval "foreach (ET\Builder\Packages\GlobalData\GlobalData::get_global_colors() as \$id => \$c) { if ((\$c['label'] ?? '') === '$1') { echo \$id; } }" +} +variable_field() { # $1 = label, $2 = field + wp eval "foreach (ET\Builder\Packages\GlobalData\GlobalData::get_global_variables() as \$items) { foreach ((array) \$items as \$v) { \$v = (array) \$v; if ((\$v['label'] ?? '') === '$1') { echo \$v['$2'] ?? ''; } } }" +} + +# 1. Create a color: dry run, confirm, stale design_rev, stored in Divi. +NEW=$(jq -cn --arg l "$PREFIX Holz" '{items: [{label: $l, color: "#5b3a29"}]}') +DONE=$(confirm lit/divi-colors-set "$NEW") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "colors create: $DONE" +COLORS_SNAPSHOT=$(echo "$DONE" | jq -r '.snapshot_id') +[ "$(stored_color "$PREFIX Holz")" = "#5b3a29" ] || fail "color not stored" +STALE=$(ability lit/divi-colors-set "$(echo "$NEW" | jq -c '.items[0].label += " 2" | . + {confirm: true, design_rev: "veraltet"}')") +[ "$(error_code "$STALE")" = "rev_mismatch" ] || fail "stale design_rev: $STALE" +pass "color created ($COLORS_SNAPSHOT), stale design_rev refused" + +# 2. A relative color from a placeholder; a cycle is refused. +REL=$(jq -cn --arg l "$PREFIX Holz hell" --arg b "{{color:$PREFIX Holz}}" \ + '{items: [{label: $l, color: $b, relative: {lightness: 20}}]}') +DONE=$(confirm lit/divi-colors-set "$REL") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "relative color: $DONE" +BASE_ID=$(color_id "$PREFIX Holz") +stored_color "$PREFIX Holz hell" | grep -q "$BASE_ID" || fail "relative color does not reference $BASE_ID" +CYCLE=$(jq -cn --arg l "$PREFIX Holz" --arg b "{{color:$PREFIX Holz hell}}" \ + '{items: [{id: $l, color: $b, relative: {lightness: 5}}]}') +[ "$(error_code "$(ability lit/divi-colors-set "$CYCLE")")" = "validation_failed" ] || fail "cycle not refused" +pass "relative color stored, cycle refused" + +# 3. A main color changes the customizer option and reads back at once. +OLD_PRIMARY=$(wp eval 'echo et_get_option("accent_color", "#2ea3f2");') +NEW_PRIMARY="#123456"; [ "$OLD_PRIMARY" = "$NEW_PRIMARY" ] && NEW_PRIMARY="#654321" +DONE=$(confirm lit/divi-colors-set "$(jq -cn --arg c "$NEW_PRIMARY" '{items: [{id: "primary", color: $c}]}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "main color: $DONE" +[ "$(wp eval 'echo et_get_option("accent_color");')" = "$NEW_PRIMARY" ] || fail "accent_color not written" +pass "main color written to the customizer option" + +# 4. Deactivating a color is a status change, not a removal. +DONE=$(confirm lit/divi-colors-set "$(jq -cn --arg l "$PREFIX Holz hell" '{items: [{id: $l, status: "inactive"}]}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "deactivate: $DONE" +wp eval "foreach (ET\Builder\Packages\GlobalData\GlobalData::get_global_colors() as \$c) { if ((\$c['label'] ?? '') === '$PREFIX Holz hell') { exit(\$c['status'] === 'inactive' ? 0 : 1); } } exit(1);" \ + || fail "color not inactive" +pass "color deactivated" + +# 5. Variables: create a number variable, archive it (never deleted, V1). +VAR=$(jq -cn --arg l "$PREFIX Radius" '{items: [{type: "numbers", label: $l, value: "12px"}]}') +DONE=$(confirm lit/divi-variables-set "$VAR") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "variable create: $DONE" +VARIABLES_SNAPSHOT=$(echo "$DONE" | jq -r '.snapshot_id') +[ "$(variable_field "$PREFIX Radius" value)" = "12px" ] || fail "variable not stored" +VAR_ID=$(variable_field "$PREFIX Radius" id) +case "$VAR_ID" in gvid-??????????) ;; *) fail "variable id format: $VAR_ID" ;; esac +pass "variable created ($VARIABLES_SNAPSHOT, $VAR_ID)" + +# 6. A page uses the variable; detach replaces the reference by its value. +PAGE_ID=$(wp post create --post_type=page --post_status=draft --post_title="$PREFIX Seite" --post_content="$(wp eval " + \$ref = '\$variable(' . wp_json_encode(['type' => 'content', 'value' => ['name' => '$VAR_ID', 'settings' => new stdClass()]]) . ')\$'; + \$text = ['content' => ['innerContent' => ['desktop' => ['value' => 'Hallo']]], + 'module' => ['decoration' => ['spacing' => ['desktop' => ['value' => ['padding' => ['top' => \$ref]]]]]]]; + echo serialize_block(['blockName' => 'divi/placeholder', 'attrs' => [], 'innerBlocks' => [ + ['blockName' => 'divi/section', 'attrs' => [], 'innerBlocks' => [ + ['blockName' => 'divi/row', 'attrs' => [], 'innerBlocks' => [ + ['blockName' => 'divi/column', 'attrs' => ['module' => ['decoration' => ['sizing' => ['desktop' => ['value' => ['flexType' => '24_24']]]]]], 'innerBlocks' => [ + ['blockName' => 'divi/text', 'attrs' => \$text, 'innerBlocks' => [], 'innerHTML' => '', 'innerContent' => []], + ], 'innerHTML' => '', 'innerContent' => [null]], + ], 'innerHTML' => '', 'innerContent' => [null]], + ], 'innerHTML' => '', 'innerContent' => [null]], + ], 'innerHTML' => '', 'innerContent' => [null]]); +")" --porcelain) +wp post get "$PAGE_ID" --field=post_content | grep -q "$VAR_ID" || fail "page does not reference the variable" +DETACH=$(jq -cn --arg v "$VAR_ID" --argjson p "$PAGE_ID" '{variable: $v, post_ids: [$p]}') +DRY=$(ability lit/divi-design-detach "$DETACH") +[ "$(error_code "$DRY")" = "confirm_required" ] || fail "detach dry run: $DRY" +DONE=$(ability lit/divi-design-detach "$(echo "$DETACH" | jq -c '. + {confirm: true}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "detach: $DONE" +CONTENT=$(wp post get "$PAGE_ID" --field=post_content) +echo "$CONTENT" | grep -q "$VAR_ID" && fail "reference still in the page" +echo "$CONTENT" | grep -q '12px' || fail "value not written into the page" +pass "variable detached from the page (reference replaced by 12px)" + +# 7. Archive the variable; it stays in the option with status archived. +DONE=$(confirm lit/divi-variables-set "$(jq -cn --arg l "$PREFIX Radius" '{items: [{id: $l, status: "archived"}]}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "archive: $DONE" +[ "$(variable_field "$PREFIX Radius" status)" = "archived" ] || fail "variable not archived" +pass "variable archived, not removed" + +# 8. Restore the variables snapshot taken before step 5: the variable created later stays, archived (V1). +LIST=$(ability lit/divi-design-snapshots '{"area": "variables"}') +echo "$LIST" | jq -e --arg s "$VARIABLES_SNAPSHOT" 'any(.snapshots[]; .id == $s)' >/dev/null || fail "snapshot not listed: $LIST" +# Activate it again first: restoring while it is already archived would be a no-op without confirm_required. +DONE=$(confirm lit/divi-variables-set "$(jq -cn --arg l "$PREFIX Radius" '{items: [{id: $l, status: "active"}]}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "reactivate: $DONE" +[ "$(variable_field "$PREFIX Radius" status)" = "active" ] || fail "variable not active again" +DONE=$(confirm lit/divi-design-restore "$(jq -cn --arg s "$VARIABLES_SNAPSHOT" '{snapshot_id: $s}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "variables restore: $DONE" +[ "$(variable_field "$PREFIX Radius" status)" = "archived" ] || fail "variable lost by the restore" +pass "variables restored, the later variable stays archived" + +# 9. Restore the colors snapshot taken before step 1: the colors of this run are gone, the main color is back. +DONE=$(confirm lit/divi-design-restore "$(jq -cn --arg s "$COLORS_SNAPSHOT" '{snapshot_id: $s}')") +[ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "colors restore: $DONE" +[ -z "$(color_id "$PREFIX Holz")" ] || fail "restored colors still hold the new color" +[ "$(wp eval 'echo et_get_option("accent_color", "#2ea3f2");')" = "$OLD_PRIMARY" ] || fail "main color not restored" +pass "colors restored (new colors gone, main color back)" + +finish