Перейти к содержимому

WP2Turbo-блоки

Имена пакетов в этом monorepo: skin = wp2lms-turbo-blocks-mos, тема = wp2lms-turbo-mos-theme. Ядро Turbo — onepix/wp2lms/src/Modules/Turbo (не пакет monorepo). Устаревшие упоминания wp2lms-turbo-core / wp2lms-turbo-blocks ниже трактовать как: core → onepix wp2lms Turbo; blocks → cd wp2lms-turbo-blocks-mos && npm run build. Pulse — в onepix.

Как добавлять собственные Gutenberg-блоки wp2turbo/* по образцу wp2turbo/tabs (интерактив) или wp2turbo/icon-list (статический список).

Эталонные реализации:

  • Интерактив: wp2lms-turbo-blocks/blocks/tabs + blocks/tab-panel → Alpine + Tabs.twig
  • Проверка знаний (серия быстрых вопросов, не банк): blocks/speed-test + speed-test-question / answers / option → Alpine + SpeedTest.twig (chrome как у банка; вопросы как choice single без feedback; customWidget)
  • Статический список с иконками: blocks/icon-list + blocks/icon-list-item → ListBlock.twig, без Alpine
  • Активны плагины WP2LMS Turbo и WP2LMS Turbo Blocks (Pulse регистрирует блоки на хуке wp2turbo/init).
  • Сборка из корня monorepo: npm run build:all (или build:core → build:skin).
  • Для интерактива на фронте нужен Twig-кomponent с "requiresAlpine": true в wp2lms-turbo/components/<Name>/schema.json.
Задача Достаточно?
Скопировать паттерн tabs (parent + child, mapping, Alpine) Да
Скопировать паттерн icon-list (parent + child, статический список, иконки) Да — см. § «Эталон: wp2turbo/icon-list»
Одиночный wp2turbo-блок без inner blocks Да — см. § «Parent + child vs одиночный»
Новый Twig-кomponent с нуля (не Tabs/Accordion) Нет — дополнительно turbo README: make-component.php, schema.json
Только стили / Alpine на существующем компоненте Нет — обзор Turbo

Сценарий Решение
Нужен интерактив в турбо-режиме, но в Gutenberg удобнее свой UI wp2turbo/* + editor UI в shared bundle
Достаточно core-блока + существующий Twig-комponent Обычный mapping в turbo, без wp2turbo
Сложная вложенность (панели, элементы списка) Parent + child блоки

Правило: платформа — Turbo в wp2lms (src/Modules/Turbo), конкретные блоки — addon (обычно wp2lms-turbo-blocks-mos).


Перед реализацией выберите модель. От неё зависят количество block.json, JS-файлов и сложность transformer.

Признак Пример
У блока есть повторяющиеся части с собственными заголовками/атрибутами Вкладки, шаги, пункты карусели
Части нужно добавлять/удалять/переставлять в редакторе «Добавить вкладку»
У каждой части свой InnerBlocks (контент внутри) Панель вкладки с paragraph, image, …
В transformer parent собирает массив из children items[] из wp2turbo/tab-panel

Эталон: wp2turbo/tabs + wp2turbo/tab-panel.

Что пишем: два (или больше) пакета в blocks/, providesContext / usesContext, child через registerBlockType, mapping только на parent.

Признак Пример
Все настройки укладываются в атрибуты одного block.json CTA с текстом кнопки и ссылкой
Контент — один InnerBlocks с обычными core-блоками Обёртка «секция турбо» с произвольным содержимым
Нет отдельных сущностей «элемент списка» в inserter Простой одиночный блок
Transformer читает attrs parent и/или один HTML-поток Без обхода typed children

Что пишем: одна папка blocks/my-block/, один BlockMapping, registerTurboBlock с простым edit/save.

Parent + child Одиночный
Папок в blocks/ 2+ 1
Inserter Только parent Один блок
BlockMapping Только parent Один
Child в allowlist Авто через turboChildBlocks() —
Сложность JS Выше (context, template, UI списка) Ниже
Когда сомневаетесь Есть «элементы» с заголовком Достаточно полей в sidebar

Допустимо не заводить wp2turbo/*-item, если «дети» — обычные core-блоки:

useInnerBlocksProps({}, { allowedBlocks: ['core/paragraph', 'core/image'], templateLock: false });

Transformer тогда собирает контент из $innerBlocks как HTML или по blockName, без отдельного wp2turbo-child. Используйте parent+child только когда нужен свой тип дочернего блока с attrs (как title у tab-panel).


flowchart TB
subgraph editor [Gutenberg editor]
A[block.json + index.js] --> B[registerTurboBlock / registerBlockType]
B --> C[shared editor styles]
end
subgraph php [PHP on init]
D[RegisterBlocks::onTurboInit] --> E[TurboBlocksRegistry]
D --> F[BlockMapping parent only]
E --> G[TurboBlockRegistrar]
end
subgraph front [Turbo frontend]
H[MappedBlockRenderer] --> I[Transformer]
I --> J[Twig Tabs.twig]
J --> K[Alpine in Blocks assets]
end
editor --> php
php --> front
  1. Редактор: parent хранит InnerBlocks, child — атрибуты (например title).
  2. Save: в post_content попадает разметка parent + children.
  3. Фронт (turbo): transformer parent читает отрендеренные inner blocks и собирает props для Twig.
  4. Фронт (обычный WP): parent → stub «только для турбо», child → пустая строка.

wp2lms-turbo-blocks/
├── blocks/ # ИСХОДНИКИ (не коммитить node_modules/build сюда)
│ ├── _shared/icon-picker/ # Lucide + медиатека + renderIconPreview (не block.json)
│ ├── _shared/card-style/ # card-style-utils.js (стили карточки в редакторе)
│ ├── my-block/
│ │ ├── block.json # wp2turbo/my-block (parent)
│ │ └── src/index.js
│ └── my-item/
│ ├── block.json # wp2turbo/my-item (child)
│ └── src/index.js
├── assets/blocks/ # СБОРКА webpack → сюда смотрит PHP
│ ├── _shared/icon-picker/ # window.wp2lmsTurboIconPicker
│ ├── my-block/{block.json,index.js,index.asset.php}
│ └── my-item/…
├── src/
│ ├── Blocks/
│ │ ├── RegisterBlocks.php # registerPath + BlockMapping + inserter filter
│ │ └── IconPickerEditorAssets.php # handle wp2lms-turbo-icon-picker (если нужны иконки)
│ ├── Icon/TurboIconLayoutResolver.php # Опционально: layout parent для transformer
│ └── Mapping/Wp2TurboMyTransformer.php
├── components/MyComponent/ # Blocks override Twig (опционально)
│ └── MyComponent.twig
├── webpack.blocks.config.cjs
└── bin/
├── patch-block-assets.php # shared + handles из block.json → index.asset.php
└── generate-lucide-register.php # vendor/lucide-register.js для icon-picker

Turbo core (общее для всех wp2turbo-блоков):

wp2lms-turbo/blocks/src/shared/
├── register-turbo-block.js # registerTurboBlock()
├── icon-size.js # ICON_SIZE_OPTIONS, resolveIconSizePx (для icon-list и др.)
├── icon-layout/ # Общие утилиты стилей/обёртки иконок (icon-list, card-header)
│ ├── icon-style-utils.js
│ ├── icon-wrap-utils.js
│ └── icon-color-settings.js
├── tabs-list.jsx # UI списка вкладок в редакторе
├── block-editor.scss # стили редактора wp2turbo-блоков
└── index.js → blocks/build/shared.js (window.wp2lmsTurboBlocks)

Файл Назначение
blocks/tabs/block.json Parent: providesContext, activeTab с "role": "local"
blocks/tabs/src/index.js InnerBlocks, TabsList, registerTurboBlock(...)
blocks/tab-panel/block.json Child: "parent": ["wp2turbo/tabs"], "usesContext", "reusable": false
blocks/tab-panel/src/index.js registerBlockType; показ/скрытие по context
src/Mapping/Wp2TurboTabsTransformer.php Сборка items[]; is-style-* → orientation + className
components/Tabs/Tabs.twig Разметка + Alpine; props.className / props.orientation
src/Blocks/RegisterBlocks.php Path, mapping, позиция в inserter

Parent:

{
"name": "wp2turbo/tabs",
"category": "wp2lms-turbo",
"attributes": {
"activeTab": { "type": "number", "default": 0, "role": "local" }
},
"providesContext": { "wp2turbo/activeTab": "activeTab" },
"editorScript": ["wp2lms-turbo-blocks-shared", "file:./index.js"],
"editorStyle": "wp2lms-turbo-blocks-shared"
}
  • "role": "local" — атрибут только для редактора, не сериализуется на фронт.
  • Shared handles обязательны — без них нет registerTurboBlock и editor-стилей.

Child:

{
"name": "wp2turbo/tab-panel",
"parent": ["wp2turbo/tabs"],
"usesContext": ["wp2turbo/activeTab"],
"supports": { "reusable": false }
}

Интерактив-перевёртыш (после Табов в «Интерактивы»): grid-контейнер с общими стилями.

Файл Назначение
blocks/flip-cards Root: columns 2–4, cardStyle/цвета/hint/layoutPreset, providesContext, appender
blocks/flip-card Child «Карточка»: parent: flip-cards, activeFace (role: local), template front+back
blocks/flip-card-front Context-стили; медиа через attrs (mediaUrl) + сайдбар; дети: card-header, card-content; hint RichText на root
blocks/flip-card-back Context layout/фон; MediaLibrary; InnerBlocks
src/Mapping/Wp2TurboFlipCardsTransformer.php Root: items[] + shared styles
src/Mapping/Wp2TurboFlipCard*.php Card/faces — _passthrough
components/FlipCards/FlipCards.twig Grid + include FlipCard/FlipCard.twig
components/FlipCard/FlipCard.twig 3D flip + Alpine turboFlipCard
assets/blocks/flip-card-back/*.svg Дефолтные иконка/картинка оборота

card-header / card-content: "parent": ["wp2turbo/card", "wp2turbo/flip-card-front"], context wp2turbo/flipCardFace скрывает UI иконок.

Второй эталон parent+child: статический список без Alpine, переиспользование Twig ListBlock.

Файл Назначение
blocks/icon-list/block.json Parent: providesContext (размер/стиль/цвета иконки), styles в block.json
blocks/icon-list/src/index.js InnerBlocks, registerTurboBlock, sidebar (размер, цвета, выравнивание)
blocks/icon-list-item/block.json Child: usesContext, attrs иконки + text, wp2lms-turbo-icon-picker
blocks/icon-list-item/src/index.js registerBlockType, RichText, IconSourceControls, renderIconPreview
blocks/_shared/icon-picker/ Lucide + медиатека (~1 МБ), отдельный webpack entry
src/Mapping/Wp2TurboIconListTransformer.php items[] из child; rich text через wp_kses_post()
src/Icon/TurboIconLayoutResolver.php Размер, is-style-*, цвета, CSS vars для списка
components/ListBlock/ListBlock.twig Blocks override; props.items, props.listStyle
src/Blocks/RegisterBlocks.php Mapping → ListBlock; inserter в «Базовые» после core/list

Третий эталон parent+child: статическая карточка без Alpine, компонент Card.

Файл Назначение
blocks/card/block.json Parent: toggles секций, styles (standard / filled / stripe), условные цвета
blocks/card/src/index.js templateLock: 'all', синхронизация child после загрузки inner blocks
blocks/card/src/card-sync.js Фиксированный порядок секций; replace только при смене toggle
blocks/card-header RichText заголовок (h5) + иконка (icon-picker)
core/image (по showImage) Стандартное изображение → mapping Image (wp2turbo-image--card-media)
blocks/card-content Свободный InnerBlocks (единственный unlocked контент)
blocks/card-caption captionName, captionRole (RichText в canvas)
core/buttons + core/button (по showButton) Стандартная кнопка → mapping Buttons
src/Icon/TurboCardStyleResolver.php Стили карточки и CSS vars
src/Icon/TurboIconHtmlRenderer.php Общий рендер Lucide/медиа-иконок (list + card-header)
src/Mapping/RendersInnerBlocksTrait.php filterRenderedBlocks / innerHtml для parent transformers
src/Mapping/Wp2TurboCardTransformer.php Parent: mediaHtml / buttonHtml из отрендеренных child; imagePosition на parent
components/Card/Card.twig Layout-оболочка; вставляет HTML child-компонентов
src/Blocks/RegisterBlocks.php Mapping: card, card-header, card-content, …
icon-list card
Lock child templateLock: false, appender templateLock: 'all', toggles parent
Контент RichText в child-item Свободные блоки только в card-content
Секции Только items header / media / content / caption / button
Mapping Parent → ListBlock Parent → Card
Inserter Базовые Дизайн
  • Child-блоки: "parent": ["wp2turbo/card"], "reusable": false, lock { remove: true, move: true } (для turbo-child). core/image и core/buttons вставляются sync-ом с тем же lock.
  • Mapping в inserter — только parent wp2turbo/card; из кастомных child отдельный mapping остаётся у card-header. Media/button — стандартные core/image и core/buttons.
  • Child card-content / card-caption — без mapping (attrs/HTML в parent transformer).
  • Стили карточки в block.json: card-standard (label «Обводка»), card-filled, card-stripe.
  • Секции на turbo-фронте показываются по фактическому контенту child, не по parent toggles.
  • Стили редактора: block-editor.scss; фронт Blocks: assets/src/styles/blocks/ (.wp2turbo-card--skin). Не вешайте wp2turbo-*--skin на DOM редактора.
tabs icon-list
Фронт Alpine (requiresAlpine: true) Статическая разметка
Twig Tabs.twig ListBlock.twig
Вариации layout Панель «Стили» в InspectorControls styles в block.json (вкладка «Стили» WP)
Child attrs title (строка) text (RichText), icon / iconSource / медиатека
Shared bundle child Только wp2lms-turbo-blocks-shared + wp2lms-turbo-icon-picker
Parent → child activeTab (role: local) iconSize, iconStyle, цвета (сериализуются)

Parent (context + block styles):

{
"attributes": {
"iconSize": { "type": "number", "default": 16 },
"iconStyle": { "type": "string", "default": "circle-filled" }
},
"providesContext": {
"wp2turbo/iconSize": "iconSize",
"wp2turbo/iconStyle": "iconStyle",
"wp2turbo/iconColor": "iconColorValue"
},
"styles": [
{ "name": "icon-circle-filled", "label": "Заливка", "isDefault": true },
{ "name": "icon-circle", "label": "Обводка" },
{ "name": "icon-plain", "label": "Без фона" }
]
}

Child (RichText + icon-picker):

{
"attributes": {
"text": { "type": "string", "default": "" }
},
"usesContext": ["wp2turbo/iconSize", "wp2turbo/iconStyle", "wp2turbo/iconColor"],
"editorScript": [
"wp2lms-turbo-blocks-shared",
"wp2lms-turbo-icon-picker",
"file:./index.js"
],
"editorStyle": ["wp2lms-turbo-blocks-shared", "wp2lms-turbo-icon-picker"]
}
  • У attrs не используйте "source": "html" для RichText — иначе text не попадёт в props transformer’а.
  • Child не импортирует parent; общие утилиты — @wp2lms/turbo-blocks-shared (icon-size.js, icon-layout/*) и @wp2lms/turbo-icon-picker (renderIconPreview, IconSourceControls).
foreach ($innerBlocks as $item) {
if (($item['blockName'] ?? '') !== 'wp2turbo/icon-list-item') {
continue;
}
$props = $item['props'] ?? [];
$text = (string) ($props['text'] ?? '');
if ($text === '') {
$text = $this->extractItemTextFromHtml((string) ($item['html'] ?? '')); // fallback
}
$items[] = [
'icon' => $this->renderItemIconHtml($props, $sizePx),
'content' => \wp_kses_post($text),
];
}

Layout (размер, is-style-*, CSS vars) — в TurboIconLayoutResolver, не дублировать в transformer.

<ul class="{{ wp2turbo_classes('wp2turbo-list', 'wp2turbo-list--skin', 'wp2turbo-list--icon', props.className) }}"
{% if props.listStyle %}style="{{ props.listStyle }}"{% endif %}>
{% for item in props.items %}
<li class="turbo-list__item turbo-list__item--icon">
<span class="turbo-list__icon-wrap">{{ item.icon|raw }}</span>
<span class="turbo-list__item-text">{{ item.content|raw }}</span>
</li>
{% endfor %}
</ul>

item.content|raw допустим после wp_kses_post() в transformer — см. SECURITY.md.


  • Namespace: wp2turbo/ (проверяется TurboBlockNamespace).
  • Slug папки: kebab-case → blocks/my-widget/.
  • Context keys: wp2turbo/myKey (уникальные, с префиксом).
import { InnerBlocks, useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { registerTurboBlock } from '@wp2lms/turbo-blocks-shared';
import metadata from '../block.json';
const ALLOWED_BLOCKS = ['wp2turbo/my-item'];
const TEMPLATE = [['wp2turbo/my-item', { title: 'Item 1' }]];
function Edit({ attributes, setAttributes, clientId }) {
const blockProps = useBlockProps({ className: 'wp2turbo-my-block' });
const innerBlocksProps = useInnerBlocksProps(
{ className: 'wp2turbo-my-block__content' },
{ allowedBlocks: ALLOWED_BLOCKS, template: TEMPLATE, templateLock: false }
);
return (
<div {...blockProps}>
{/* UI редактора: заголовки, кнопки, InspectorControls */}
<div {...innerBlocksProps} />
</div>
);
}
function save() {
return <InnerBlocks.Content />;
}
registerTurboBlock(metadata, {
edit: Edit,
save,
});
  • registerBlockType, не registerTurboBlock.
  • Child не появляется в inserter — только внутри parent.
  • Читать context через prop context в edit.
  • В save() сохранять нужные data-* атрибуты для transformer (см. data-title у tab-panel).
  • Для форматированного текста: RichText в canvas + <RichText.Content /> в save; attr text без "source": "html" (см. icon-list-item).
  • Child списка: корневой элемент edit/save — <li> (семантика списка), не <div>.
  • Для иконок: @wp2lms/turbo-icon-picker, не дублировать Lucide в bundle child.

src/Blocks/RegisterBlocks.php:

$turbo->blocks()->registerPath(WP2LMS_TURBO_PULSE_PATH . 'assets/blocks');
$turbo->mappings()->register(new BlockMapping(
blockName: 'wp2turbo/my-block', // только parent
component: 'MyComponent', // имя Twig-компонента
transformerClass: Wp2TurboMyTransformer::class,
));

turbo->blocks()->registerPath(...) — path уже есть для Blocks.

При блоках с иконками в child дополнительно в RegisterBlocks::register():

IconPickerEditorAssets::register();

Mapping и inserter filter — как для tabs; для icon-list inserter: Базовые после core/list (см. insertBasicBlock в RegisterBlocks.php).

Mapping регистрируется только для parent. Child попадает в allowlist автоматически через TurboBlocksRegistry::childBlockNames().

final class Wp2TurboMyTransformer implements PropsTransformerInterface
{
public function transform(array $block, array $innerBlocks = [], string $blockContent = ''): array
{
$items = [];
foreach ($innerBlocks as $inner) {
if (($inner['blockName'] ?? '') !== 'wp2turbo/my-item') {
continue;
}
$items[] = [
'title' => (string) ($inner['props']['title'] ?? ''), // attrs child без mapping
'content' => (string) ($inner['html'] ?? ''),
];
}
if ($items === []) {
return ['content' => $blockContent]; // fallback passthrough
}
return ['items' => $items, 'active' => 0];
}
}

Сверяйте props со schema.json / Twig (components/MyComponent/).

Child без mapping: в $innerBlocks поле props для wp2turbo-child — это attrs из block.json (см. MappedBlockRenderer::renderInnerBlocks). Отдельный transformer для child не нужен, если parent сам собирает items.

Card: mapping на child с passthrough — card-header регистрирует BlockMapping для рендера Twig (Heading). Parent Wp2TurboCardTransformer вставляет готовый HTML child и HTML от core/image / core/buttons в Card.twig.

Alpine (если блок интерактивный на фронте)

  1. schema.json компонента: "requiresAlpine": true (для Tabs уже есть в turbo).
  2. Модуль wp2lms-turbo-blocks/assets/src/frontend/my-block.js + Alpine.data('turboMyBlock', …).
  3. Подключение в assets/src/entries/turbo.js (import + вызов register-функции).
  4. В Twig: x-data="turboMyBlock({ … })" — см. components/Tabs/Tabs.twig.
  5. После правок: npm run build --prefix wp2lms-turbo-blocks (vite собирает alpine-бандл).

Новый компонент vs переиспользование

Подход Когда
Переиспользовать Tabs / Accordion / … Поведение и разметка совпадают с существующим Twig
Новый component в turbo + override в Blocks Нужна другая структура props или a11y-паттерн — правки в onepix/wp2lms/src/Modules/Turbo
  • Базовый Twig может быть в wp2lms-turbo/components/MyComponent/.
  • Override скина: wp2lms-turbo-blocks/components/MyComponent/MyComponent.twig.
  • Интерактив: assets/src/frontend/*.js + x-data в Twig.
  • A11y: см. PENGUIN_REFERENCE.md (tabs, accordion, modal).

У блоков с несколькими layout-вариантами нужны согласованные три слоя: редактор, transformer, фронт.

Два способа в редакторе — выберите один:

Способ Когда Эталон
styles в block.json Варианты — стандартные block styles WP (is-style-*), переключатель во вкладке «Стили» icon-list
Панель в InspectorControls Кастомный UI, несколько layout-классов, не block styles API Устаревший пример; сейчас tabs тоже через styles в block.json

Общие правила для обоих:

  • варианты храните в attributes.className классами is-style-{name};
  • одновременно активен только один layout-класс из набора блока;
  • transformer и Twig должны понимать те же is-style-*.

Способ A — styles в block.json (icon-list):

"styles": [
{ "name": "icon-circle-filled", "label": "Заливка", "isDefault": true },
{ "name": "icon-circle", "label": "Обводка" },
{ "name": "icon-plain", "label": "Без фона" }
]

В JS синхронизируйте attributes.iconStyle с className (см. resolveIconStyleFromClassName в @wp2lms/turbo-blocks-shared/icon-layout).

Способ B — панель «Стили» в sidebar (legacy):

Раньше tabs использовали кастомную панель (TabLayoutStyles). Сейчас layout вкладок задаётся через styles в block.json (is-style-left, is-style-top, …) — тот же механизм, что у icon-list. Кастомную панель оставляйте только если block styles API не подходит.

Для preview в редакторе добавьте селекторы в wp2lms-turbo/blocks/src/shared/block-editor.scss (см. .wp2turbo-tabs.is-style-left).

Transformer — проброс на фронт

  • читайте className из $block['attrs'];
  • при необходимости нормализуйте в явный prop (orientation, variant, …) и сохраняйте is-style-* в className для CSS;
  • эталон: Wp2TurboTabsTransformer::resolveOrientation() + mergeLayoutClassName().

Twig + CSS — рендер вариации

  • передавайте className в корень через wp2turbo_classes(..., props.className|default(''));
  • layout-специфичные props (например orientation) — для a11y и Alpine;
  • стили вариантов — в Blocks assets/src/styles/blocks/ по .wp2turbo-my-block--skin.is-style-* (см. .wp2turbo-tabs--skin.is-style-left).
<div class="{{ wp2turbo_classes('wp2turbo-tabs', 'wp2turbo-tabs--skin', props.className|default('')) }}" …>

Alpine может дополнительно читать класс с корня (tabs.js: classList.contains('is-style-left')).

Чеклист вариаций

Слой Что проверить
Редактор Панель «Стили», переключение меняет preview
Save В post_content в className блока есть выбранный is-style-*
Transformer Вариант не теряется; props для Twig согласованы
Twig / CSS Каждый is-style-* визуально отличается на turbo-странице
Smoke При наличии — assert в bin/smoke-wp2turbo-blocks.php

Если у блока один layout — вкладку «Стили» можно не делать; для блоков с альтернативной раскладкой (tabs сверху/слева, accordion compact/full и т.п.) — обязательно.

По умолчанию parent-блок регистрируется в категории WP2LMS Turbo (wp2lms-turbo):

  • в block.json: "category": "wp2lms-turbo";
  • в JS: registerTurboBlock() принудительно ставит ту же категорию через shared bundle.

Child-блоки в inserter не показываются — только внутри parent.

При включённой опции «Переупорядочить блоки» parent можно вынести в тематический раздел (Базовые, Интерактивы, Дизайн, …), если блок логичнее стоять рядом с похожими core-блоками.

Для этого в addon добавьте фильтр wp2turbo/editor/block_registry и вставьте имя блока в список blocks нужной категории. Пример — wp2turbo/tabs в «Интерактивы» после core/accordion:

add_filter('wp2turbo/editor/block_registry', [self::class, 'registerInserterBlock'], 5);
public static function registerInserterBlock(array $config): array
{
$settings = new SettingsRepository();
if (! $settings->shouldReorderBlockCategories()) {
return $config;
}
$blocks = &$config['categories'][EditorBlockRegistry::CATEGORY_INTERACTIVE]['blocks'];
$blockName = 'wp2turbo/tabs';
if (! in_array($blockName, $blocks, true)) {
$accordionIndex = array_search('core/accordion', $blocks, true);
$insertAt = $accordionIndex !== false ? $accordionIndex + 1 : count($blocks);
array_splice($blocks, $insertAt, 0, [$blockName]);
}
return $config;
}

Константы категорий — в wp2lms-turbo/src/Editor/EditorBlockRegistry.php (CATEGORY_BASIC, CATEGORY_INTERACTIVE, CATEGORY_DESIGN, …).

TurboBlockRegistrar и EditorBlockScripts подхватят категорию из реестра при reorder. Child-блоки в этот фильтр не добавлять.

Режим Где виден parent в inserter
Reorder выкл. Вкладка WP2LMS Turbo
Reorder вкл. Тематический раздел из фильтра (или WP2LMS Turbo, если фильтр не добавлен)

Только в turbo core:

  1. Стили в wp2lms-turbo/blocks/src/shared/block-editor.scss.
  2. При необходимости — React-хелпер рядом с tabs-list.jsx.
  3. Экспорт из blocks/src/shared/index.js.
  4. npm run build --prefix wp2lms-turbo/blocks.

Из корня monorepo:

Окно терминала
npm run build:all

Или по шагам:

Окно терминала
npm run build:core # shared bundle + turbo plugin
npm run build:skin # blocks package + vite + blocks plugin

Pulse отдельно:

Окно терминала
npm run build:blocks --prefix wp2lms-turbo-blocks # webpack + patch-block-assets.php
php wp2lms-turbo-blocks/bin/build-plugin.php
# Сборка / smoke — через onepix wp2lms (модуль Turbo), не пакет monorepo

Порядок важен: сначала core (shared.js), потом skin.

patch-block-assets.php после webpack:

  1. Добавляет wp2lms-turbo-blocks-shared в каждый index.asset.php.
  2. Читает editorScript из соседнего block.json и дописывает named handles (например wp2lms-turbo-icon-picker) в dependencies file-скрипта — без этого child может загрузиться раньше icon-picker и упасть с ReferenceError.

Без шага 1 блок не загрузит shared bundle.

Где Что проверить
Редактор Parent в inserter; child только внутри parent
Обычный фронт Stub у parent, пусто у child
Turbo-страница Twig; transformer props; Alpine — только если requiresAlpine
Allowlist ON Блок не исчез из редактора
/blocks/ A11y по PRODUCTION_CHECKLIST.md

Smoke transformer (turbo):

Окно терминала
# smoke transformers — в onepix/wp2lms (модуль Turbo)

Если по таблице выше выбран одиночный вариант:

  1. Одна папка blocks/my-block/ (block.json + src/index.js).
  2. registerTurboBlock + edit/save:
    • либо статическая разметка + attrs в InspectorControls;
    • либо один InnerBlocks для произвольного core-контента (без wp2turbo/my-item).
  3. Один BlockMapping + transformer (props из $block['attrs'], $blockContent, опц. $innerBlocks).
  4. mapping.json не нужен — регистрация программная в RegisterBlocks.php.

Не путать с parent+child: если появляется отдельный тип «элемент» с собственным block.json — переходите на модель Tabs.


Механизм Поведение
registerTurboBlock / block.json Категория по умолчанию: wp2lms-turbo
wp2turbo/editor/block_registry При reorder — перенос parent в тематический раздел + порядок внутри него
TurboBlockRegistrar::assignTurboBlocks Назначает категорию из реестра, если reorder включён
EditorBlockScripts JS-дубль смены category для уже зарегистрированных wp2turbo-блоков
UnsupportedBlocksDisabler Parent в allowlist через mapping; child — через turboChildBlocks()
TurboBlockRegistrar Child не добавляется в inserter
TurboBlockFrontendFilter Stub / empty вне turbo-режима

  • Не добавлять child в EditorBlockRegistry / inserter filter.
  • "reusable": false — чтобы не выносили из parent.
  • Mapping на child — только осознанное исключение (card: header/media/button с passthrough).
  • Parent всегда объявляйте с "category": "wp2lms-turbo" — это fallback, когда reorder выключен.
  • Фильтр block_registry для тематического раздела проверяйте через SettingsRepository::shouldReorderBlockCategories().
  • В фильтр добавляйте только parent, не child. В RegisterBlocks используйте один insertBlockAfter().
  • Не храните layout только в локальном state React — он не попадёт на фронт; используйте className / attrs блока.
  • Не забывайте стили вариантов и в block-editor.scss (редактор), и в assets/src/styles/blocks/ (turbo-фронт).
  • Transformer и Twig должны понимать одни и те же is-style-* классы.
  • Не класть node_modules / webpack output в blocks/ исходников — только в assets/blocks/.
  • Webpack externals:
    • '@wp2lms/turbo-blocks-shared': 'wp2lmsTurboBlocks'
    • '@wp2lms/turbo-icon-picker': 'wp2lmsTurboIconPicker' (только для блоков с иконками)
  • После block.json изменений — пересобрать blocks и build-plugin.php.

Для блоков с выбором иконки (например icon-list-item) Lucide и UI picker вынесены в отдельный bundle:

Путь Назначение
blocks/_shared/icon-picker/ Исходники: LucideIconPicker, IconMediaPicker, IconSourceControls, render-icon-preview.jsx, icon-utils.js
assets/blocks/_shared/icon-picker/ Сборка webpack (library: wp2lmsTurboIconPicker)
src/Blocks/IconPickerEditorAssets.php Регистрация handle wp2lms-turbo-icon-picker
bin/generate-lucide-register.php Генерирует vendor/lucide-register.js в _shared/icon-picker

Подключение в child-блоке:

"editorScript": [
"wp2lms-turbo-blocks-shared",
"wp2lms-turbo-icon-picker",
"file:./index.js"
],
"editorStyle": [
"wp2lms-turbo-blocks-shared",
"wp2lms-turbo-icon-picker"
]

Стили picker — в bundle _shared/icon-picker, отдельный file:./index.css у child не нужен.

В JS:

import { IconSourceControls, resolveIconName, renderIconPreview } from '@wp2lms/turbo-icon-picker';
import {
resolveIconSizePx,
getIconWrapClassName,
resolveIconStyleFromClassName,
} from '@wp2lms/turbo-blocks-shared';

webpack.blocks.config.cjs пропускает папки с префиксом _ при auto-discovery блоков и собирает _shared/icon-picker отдельным entry.

Путь Назначение
wp2lms-turbo/blocks/src/shared/icon-layout/ icon-style-utils, icon-wrap-utils, icon-color-settings → @wp2lms/turbo-blocks-shared
src/Icon/TurboIconHtmlRenderer.php PHP-рендер иконок для list + card-header
src/Mapping/RendersInnerBlocksTrait.php Общий код фильтрации inner blocks

Логика размера/стиля/цветов списка вынесена в TurboIconLayoutResolver.php; transformer только собирает items. Размер иконки в JS — icon-size.js + icon-layout/* (экспорт через @wp2lms/turbo-blocks-shared).

Parent передаёт context (wp2turbo/iconSize, wp2turbo/iconStyle, цвета) в child; child не импортирует parent. Fallback iconStyle в child — ICON_STYLE_DEFAULT (circle-filled), не plain.

  • Не добавлять wp2lms-turbo-icon-picker в dependencies wp2lms-turbo-blocks-shared — циклическая зависимость и исчерпание памяти в WP_Dependencies.
  • Не линковать icon-picker на shared через PHP-хук «на все handles блока» — только через index.asset.php (patch-block-assets).
  • Не использовать "source": "html" у RichText-attrs child — текст не попадёт в $innerBlocks[].props.
  • Не импортировать parent из child (../../icon-list/...) — только shared bundles.
  • После смены block.json editorScript — пересобрать blocks (patch-block-assets.php обновит deps).
  • role: local атрибуты не ждать в transformer с сохранённого HTML.
  • Стили редактора: block-editor.scss (turbo), не Blocks assets/src/styles/blocks/.
  • Классы turbo-*--skin — только на turbo-фронте (Twig + assets/src/styles/blocks/). В редакторе используйте префикс wp2turbo-* и селекторы в block-editor.scss.
  • Обёртка иконки в редакторе: граница только при стиле «Обводка» (icon-circle / is-style-icon-circle); для «Заливка» и «Без фона» — border: none в block-editor.scss (см. card-header, icon-list).
  • Пустой card-content: кнопка вставки блоков выровнена справа (.wp2turbo-card-content .block-list-appender в block-editor.scss).
  • CSS vars иконок: в JS (getIconListStyleVars) и PHP (TurboIconLayoutResolver::buildListStyle) дублируются --turbo-list-* и --wp2turbo-* для совместимости.

wp2turbo-блок может рендериться через тот же Twig, что и core-кomponent (например Tabs), но mapping для wp2turbo регистрируется программно в RegisterBlocks.php, а не через mapping.json.


Хук Назначение
wp2turbo/init Регистрация path + mappings addon
wp2turbo/blocks/register Доп. block types
wp2turbo/editor/block_registry Порядок/категории inserter
wp2turbo/editor/child_blocks Доп. child names для allowlist
wp2turbo/editor/scripts_config JS-конфиг редактора

Документ Содержание
обзор Turbo Blocks: стили, Alpine, SkinResolver
../../wp2lms-turbo/README.md Turbo: transformers, components, EditorBlockRegistry
PENGUIN_REFERENCE.md A11y-паттерны Tabs/Accordion/Modal
PRODUCTION_CHECKLIST.md QA перед релизом
SECURITY.md Trust boundary для |raw в Twig

Минимальный diff для «ещё одного блока как Tabs»

Заголовок раздела «Минимальный diff для «ещё одного блока как Tabs»»
  1. Скопировать blocks/tabs → blocks/my-block, blocks/tab-panel → blocks/my-item.
  2. Заменить wp2turbo/tabs → wp2turbo/my-block, context keys, class names.
  3. Скопировать transformer → переименовать, поправить blockName фильтры и props.
  4. Добавить BlockMapping и при необходимости фильтр block_registry в RegisterBlocks.php.
  5. Twig + CSS: вариации is-style-*; при нескольких layout — панель «Стили» в InspectorControls.
  6. npm run build:all + проверка в редакторе и на turbo-странице.

Минимальный diff для «ещё одного блока как icon-list»

Заголовок раздела «Минимальный diff для «ещё одного блока как icon-list»»
  1. Скопировать blocks/icon-list → blocks/my-block, blocks/icon-list-item → blocks/my-item.
  2. Parent: providesContext, attrs layout; child: attrs элемента + при необходимости @wp2lms/turbo-icon-picker.
  3. Transformer → items[], layout вынести в resolver при сложной логике (TurboIconLayoutResolver как образец).
  4. BlockMapping → существующий или новый Twig (ListBlock или свой component).
  5. styles в block.json или InspectorControls — не смешивать без необходимости.
  6. RichText в child: save через <RichText.Content />, transformer — wp_kses_post().
  7. npm run build:blocks --prefix wp2lms-turbo-blocks + smoke + редактор.

Минимальный diff для «ещё одного блока как card»

Заголовок раздела «Минимальный diff для «ещё одного блока как card»»
  1. Скопировать blocks/card и пять child (card-header, card-media, card-content, card-caption, card-button).
  2. Parent: toggles секций, templateLock: 'all', card-sync.js для порядка child.
  3. Child с иконками: @wp2lms/turbo-icon-picker + утилиты из @wp2lms/turbo-blocks-shared/icon-layout.
  4. card-media — только медиатека (IconMediaPicker), не Lucide.
  5. Mapping: parent → Card; child card-header / card-media / card-button — отдельные transformers с passthrough HTML.
  6. Стили карточки: styles в block.json + blocks/_shared/card-style/card-style-utils.js.
  7. npm run build:all + smoke (card, card-header, card-media, card-button).

# Файл Tabs Icon-list Card Обязательно
1 blocks/{name}/block.json + src/index.js Parent Parent Parent Parent
2 blocks/{child}/block.json + src/index.js Child Child Child ×5 Если есть child
3 src/Mapping/Wp2Turbo{Name}Transformer.php ✓ ✓ ✓ Parent mapping
4 src/Blocks/RegisterBlocks.php ✓ ✓ ✓ Path уже есть; + BlockMapping, inserter
5 components/{Component}/{Component}.twig Tabs ListBlock Card Blocks override или turbo
6 assets/src/styles/blocks/ ✓ ✓ ✓ Стили --skin + is-style-*
7 assets/src/frontend/{name}.js + turbo.js ✓ — — Если requiresAlpine
8 wp2lms-turbo/blocks/src/shared/block-editor.scss ✓ ✓ ✓ Preview в редакторе
9 assets/blocks/* ген. ген. ген. Генерируется webpack
10 blocks/_shared/icon-picker/ + IconPickerEditorAssets.php — ✓ ✓ (header, media) Child с Lucide/медиатекой
11 wp2lms-turbo/blocks/src/shared/icon-layout/ — ✓ ✓ Утилиты стилей иконок
12 blocks/_shared/card-style/card-style-utils.js — — ✓ Стили карточки в JS
13 src/Icon/*LayoutResolver.php + TurboIconHtmlRenderer.php — ✓ ✓ Layout + рендер иконок в PHP
14 bin/generate-lucide-register.php — ✓ ✓ Перед сборкой icon-picker

Webpack подхватывает новую папку в blocks/ автоматически; папки _shared/ — нет (отдельный entry в webpack.blocks.config.cjs).


Трёхфазный рефакторинг wp2turbo-блоков (бэкап: _backups/refactor-blocks-20260713-*):

Фаза Изменения
HIGH icon-layout/* в @wp2lms/turbo-blocks-shared; renderIconPreview в icon-picker; TurboIconHtmlRenderer.php, RendersInnerBlocksTrait; убраны дубликаты icon-style-utils / icon-wrap-utils в блоках
MEDIUM card-style-utils.js; RegisterBlocks::insertBlockAfter(); card-media → alias @wp2lms/turbo-icon-picker; удалены legacy fallbacks в Wp2TurboCardMediaTransformer
LOW icon-list-item edit на <li>; fallback ICON_STYLE_DEFAULT; унификация CSS vars; удалён мёртвый код (card-sync TEMPLATE, tabLayout attr)

Smoke после рефакторинга: transformers OK (модуль Turbo в wp2lms).