Skip to content

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

Предварительные условия

Section titled “Предварительные условия”
  • Активны плагины 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.

Когда этого документа достаточно

Section titled “Когда этого документа достаточно”
Задача Достаточно?
Скопировать паттерн 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).


Parent + child vs одиночный блок

Section titled “Parent + child vs одиночный блок”

Перед реализацией выберите модель. От неё зависят количество 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.

Когда одиночный блок (без dedicated child)

Section titled “Когда одиночный блок (без dedicated child)”
Признак Пример
Все настройки укладываются в атрибуты одного 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

Гибрид: одиночный parent + InnerBlocks

Section titled “Гибрид: одиночный parent + InnerBlocks”

Допустимо не заводить 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

Поток данных (parent + children)

Section titled “Поток данных (parent + children)”
  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

Ключевые паттерны block.json

Section titled “Ключевые паттерны block.json”

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, цвета (сериализуются)

Ключевые паттерны block.json

Section titled “Ключевые паттерны block.json”

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.


Пошаговый чеклист: новый parent + child

Section titled “Пошаговый чеклист: новый parent + child”
  • 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).

6.1. Вариации стилей (редактор + Twig)

Section titled “6.1. Вариации стилей (редактор + Twig)”

У блоков с несколькими 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 и т.п.) — обязательно.

7. Категория и позиция в inserter

Section titled “7. Категория и позиция в inserter”

По умолчанию 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, если фильтр не добавлен)

8. Стили редактора (если нужны)

Section titled “8. Стили редактора (если нужны)”

Только в 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)

Одиночный блок (без dedicated child)

Section titled “Одиночный блок (без dedicated child)”

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

  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-режима

Частые ошибки (не повторять)

Section titled “Частые ошибки (не повторять)”
  • Не добавлять 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.

Shared icon-picker (Lucide + медиатека)

Section titled “Shared icon-picker (Lucide + медиатека)”

Для блоков с выбором иконки (например 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.

icon-list / icon-picker — частые ошибки

Section titled “icon-list / icon-picker — частые ошибки”
  • Не добавлять 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-* для совместимости.

Переиспользование Twig-компонента

Section titled “Переиспользование Twig-компонента”

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»

Section titled “Минимальный 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»

Section titled “Минимальный 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»

Section titled “Минимальный 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).

Сводка: файлы для нового wp2turbo-блока

Section titled “Сводка: файлы для нового wp2turbo-блока”
# Файл 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).


История рефакторинга (2026-07)

Section titled “История рефакторинга (2026-07)”

Трёхфазный рефакторинг 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).