From 26108bc9a975b9c22662306286858eed3a0f8ffb Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sat, 26 Sep 2026 19:20:23 +0200 Subject: [PATCH] feat: short-form level for every module with a heading title (trial) contact-form, cta, toggle, accordion-item, slide, number-counter and team-member (name) take level like heading/blurb; paths from Divi's conversion outlines, checked live with divi-shortform-check.php. A test keeps the guide's field table in sync with ShortformMap. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- docs/guides/divi-shortform.md | 18 +++++---- src/Integrations/Divi/Schema/ShortformMap.php | 24 +++++++++++- .../Divi/Schema/ModuleCatalogTest.php | 37 +++++++++++++++++++ 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/docs/guides/divi-shortform.md b/docs/guides/divi-shortform.md index f9b40c3..e6a2155 100644 --- a/docs/guides/divi-shortform.md +++ b/docs/guides/divi-shortform.md @@ -31,7 +31,9 @@ Neue Divi-Inhalte beschreibt man als JSON-Knoten. `lit-divi-insert` (in `nodes`) Icon-Liste) bekommen ihre Einträge in `items`; der Eintragstyp ergibt sich von selbst. - `label` fehlt → Admin-Label „ – “, z. B. „Hero – Button“; Zeilen „Hero – Zeile“, eine Zeile in einer Spalte „Hero – Zeile (innen)“. Abschnitten deshalb ein Label geben. -- `heading` ohne `level` wird `h2` (eine Seite hat meist schon eine `h1` im Titel). +- `heading` ohne `level` wird `h2` (eine Seite hat meist schon eine `h1` im Titel). Auch Module mit Titel + (`blurb`, `cta`, `toggle`, Akkordeon-Eintrag, Slide, `contact-form`, `number-counter`, `team-member`) nehmen + `level` (`h1`–`h6`); ohne `level` gilt Divis Standard (beim Kontaktformular `h1` – daher meist `h2` setzen). ## Kurzform-Felder @@ -42,20 +44,20 @@ Neue Divi-Inhalte beschreibt man als JSON-Knoten. `lit-divi-insert` (in `nodes`) | `button` | `text`, `url`, `new_window` (true/false) | | `image` | `image_id` (Mediathek, setzt URL/ID/Alt), `src`, `alt`, `title`, `url` | | `blurb` | `title`, `url`, `content`, `image_id`, `src`, `alt`, `level` | -| `cta` | `title`, `content`, `button_text`, `button_url` | +| `cta` | `title`, `content`, `button_text`, `button_url`, `level` | | `icon` | `url`, `title` | | `divider` | `show`, `color` | | `testimonial` | `author`, `job_title`, `company`, `company_url`, `content`, `portrait_id`, `portrait_url` | -| `team-member` | `name`, `position`, `content`, `image_id`, `image_url` | -| `accordion` → `accordion-item` | Eintrag: `title`, `content`, `open` | +| `team-member` | `name`, `position`, `content`, `image_id`, `image_url`, `level` (des Namens) | +| `accordion` → `accordion-item` | Eintrag: `title`, `content`, `open`, `level` | | `tabs` → `tab` | Eintrag: `title`, `content` | -| `toggle` | `title`, `content`, `open` | +| `toggle` | `title`, `content`, `open`, `level` | | `gallery` | `image_ids` (Liste von Anhang-IDs) | -| `slider` → `slide` | Eintrag: `title`, `content`, `button_text`, `button_url`, `image_id`, `src`, `alt` | -| `contact-form` → `contact-field` | Formular: `title`, `email`, `success_message`, `submit_text`; Feld: `title`, `id`, `type` (`input`, `email`, `text`, `checkbox`, `radio`, `select`), `required` | +| `slider` → `slide` | Eintrag: `title`, `content`, `button_text`, `button_url`, `image_id`, `src`, `alt`, `level` | +| `contact-form` → `contact-field` | Formular: `title`, `level`, `email`, `success_message`, `submit_text`; Feld: `title`, `id`, `type` (`input`, `email`, `text`, `checkbox`, `radio`, `select`), `required` | | `map` → `map-pin` | Karte: `address`, `lat`, `lng`, `zoom`; Pin: `title`, `content`, `address`, `lat`, `lng` | | `video` | `src`, `thumbnail` | -| `number-counter` | `number`, `title`, `percent` | +| `number-counter` | `number`, `title`, `percent`, `level` | | `social-media-follow` → `social-media-follow-network` | Eintrag: `network` (z. B. `facebook`), `url`, `label` | | `icon-list` → `icon-list-item` | Eintrag: `text` | | `code` | `content` – nur, wenn kein natives Modul passt (Warnung) | diff --git a/src/Integrations/Divi/Schema/ShortformMap.php b/src/Integrations/Divi/Schema/ShortformMap.php index 8c4ba1b..d6bbace 100644 --- a/src/Integrations/Divi/Schema/ShortformMap.php +++ b/src/Integrations/Divi/Schema/ShortformMap.php @@ -166,6 +166,7 @@ final class ShortformMap 'content' => $content, 'button_text' => $buttonText, 'button_url' => $buttonUrl, + 'level' => $level, ]), 'icon' => self::module(null, [ 'url' => self::field('icon.innerContent', 'url', 'url', 'Linkziel'), @@ -190,12 +191,28 @@ final class ShortformMap 'content' => $content, 'image_id' => $media('image.innerContent', ['url', null, null], 'Foto aus der Mediathek'), 'image_url' => self::field('image.innerContent', 'url', 'url', 'Foto-URL'), + 'level' => self::field( + 'name.decoration.font.font', + 'headingLevel', + 'level', + 'Überschriftenebene des Namens h1–h6' + ), ]), 'accordion' => self::module(null, [], 'accordion-item'), - 'accordion-item' => self::module('title', ['title' => $title, 'content' => $content, 'open' => $open]), + 'accordion-item' => self::module('title', [ + 'title' => $title, + 'content' => $content, + 'open' => $open, + 'level' => $level, + ]), 'tabs' => self::module(null, [], 'tab'), 'tab' => self::module('title', ['title' => $title, 'content' => $content]), - 'toggle' => self::module('title', ['title' => $title, 'content' => $content, 'open' => $open]), + 'toggle' => self::module('title', [ + 'title' => $title, + 'content' => $content, + 'open' => $open, + 'level' => $level, + ]), 'gallery' => self::module('image_ids', [ 'image_ids' => self::field('image.advanced.galleryIds', null, 'ids', 'Anhang-IDs, z. B. [12, 33]'), ]), @@ -208,9 +225,11 @@ final class ShortformMap 'image_id' => $media('image.innerContent', ['src', null, 'alt'], 'Bild aus der Mediathek'), 'src' => self::field('image.innerContent', 'src', 'url', 'Bild-URL'), 'alt' => self::field('image.innerContent', 'alt', 'text', 'Alt-Text'), + 'level' => $level, ]), 'contact-form' => self::module(null, [ 'title' => $title, + 'level' => $level, 'email' => self::field('email.advanced.receiver', null, 'text', 'Empfänger (leer = Admin-E-Mail)'), 'success_message' => self::field('module.advanced.successMessage', null, 'text', 'Text nach Absenden'), 'submit_text' => self::field('button.innerContent', 'text', 'text', 'Beschriftung Absenden'), @@ -244,6 +263,7 @@ final class ShortformMap 'number' => self::field('number.innerContent', null, 'text', 'Zahl'), 'title' => $title, 'percent' => self::field('number.advanced.enablePercentSign', null, 'onoff', 'Prozentzeichen anzeigen'), + 'level' => $level, ]), 'social-media-follow' => self::module(null, [], 'social-media-follow-network'), 'social-media-follow-network' => self::module('url', [ diff --git a/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php b/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php index a402f05..1a8eb98 100644 --- a/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php +++ b/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php @@ -202,4 +202,41 @@ final class ModuleCatalogTest extends TestCase self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent')); self::assertIsString($catalog->kinds('divi/text')['content.innerContent']); } + + /** + * Trial feedback: every short-form module with a heading title offers `level` (Divi's + * conversion outlines map header_level/title_level to .decoration.font.font headingLevel). + */ + public function testModulesWithAHeadingTitleOfferLevel(): void + { + $expected = [ + 'heading' => 'title', 'blurb' => 'title', 'cta' => 'title', 'toggle' => 'title', + 'accordion-item' => 'title', 'slide' => 'title', 'contact-form' => 'title', + 'number-counter' => 'title', 'team-member' => 'name', + ]; + foreach ($expected as $type => $element) { + $level = ShortformMap::fields($type)['level'] ?? null; + self::assertNotNull($level, $type); + self::assertSame($element . '.decoration.font.font', $level['path'], $type); + self::assertSame('headingLevel', $level['sub'], $type); + self::assertSame('level', $level['kind'], $type); + } + } + + /** The field table of the divi-shortform guide lists every short-form field. */ + public function testShortformGuideTableListsEveryField(): void + { + $guide = (string) file_get_contents(dirname(__DIR__, 5) . '/docs/guides/divi-shortform.md'); + $rows = array_values(array_filter( + explode("\n", $guide), + static fn (string $line): bool => str_starts_with($line, '| `') + )); + foreach (ShortformMap::types() as $type) { + $row = current(array_filter($rows, static fn (string $line): bool => str_contains($line, '`' . $type . '`'))); + self::assertIsString($row, $type . ' has a row'); + foreach (array_keys(ShortformMap::fields($type)) as $field) { + self::assertStringContainsString('`' . $field . '`', $row, $type . '.' . $field); + } + } + } }