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 |
Когда нужен wp2turbo-блок
Section titled “Когда нужен wp2turbo-блок”| Сценарий | Решение |
|---|---|
| Нужен интерактив в турбо-режиме, но в 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.
Когда parent + child (как Tabs)
Section titled “Когда parent + child (как Tabs)”| Признак | Пример |
|---|---|
| У блока есть повторяющиеся части с собственными заголовками/атрибутами | Вкладки, шаги, пункты карусели |
| Части нужно добавлять/удалять/переставлять в редакторе | «Добавить вкладку» |
| У каждой части свой 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.
Сравнение
Section titled “Сравнение”| 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).
Архитектура
Section titled “Архитектура”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)”- Редактор: parent хранит
InnerBlocks, child — атрибуты (напримерtitle). - Save: в post_content попадает разметка parent + children.
- Фронт (turbo): transformer parent читает отрендеренные inner blocks и собирает
propsдля Twig. - Фронт (обычный WP): parent → stub «только для турбо», child → пустая строка.
Структура файлов (Blocks)
Section titled “Структура файлов (Blocks)”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-pickerTurbo 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)Эталон: wp2turbo/tabs
Section titled “Эталон: wp2turbo/tabs”| Файл | Назначение |
|---|---|
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 }}Эталон: wp2turbo/flip-cards
Section titled “Эталон: wp2turbo/flip-cards”Интерактив-перевёртыш (после Табов в «Интерактивы»): 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 иконок.
Эталон: wp2turbo/icon-list
Section titled “Эталон: wp2turbo/icon-list”Второй эталон 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 |
Эталон: wp2turbo/card
Section titled “Эталон: wp2turbo/card”Третий эталон 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
Section titled “Отличия от icon-list”| 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
Section titled “Отличия от tabs”| 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не попадёт вpropstransformer’а. - Child не импортирует parent; общие утилиты —
@wp2lms/turbo-blocks-shared(icon-size.js,icon-layout/*) и@wp2lms/turbo-icon-picker(renderIconPreview,IconSourceControls).
Transformer: items + rich text
Section titled “Transformer: items + rich text”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”1. Именование
Section titled “1. Именование”- Namespace:
wp2turbo/(проверяетсяTurboBlockNamespace). - Slug папки:
kebab-case→blocks/my-widget/. - Context keys:
wp2turbo/myKey(уникальные, с префиксом).
2. Parent block (JS)
Section titled “2. Parent block (JS)”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,});3. Child block (JS)
Section titled “3. Child block (JS)”registerBlockType, неregisterTurboBlock.- Child не появляется в inserter — только внутри parent.
- Читать context через prop
contextвedit. - В
save()сохранять нужныеdata-*атрибуты для transformer (см.data-titleу tab-panel). - Для форматированного текста:
RichTextв canvas +<RichText.Content />в save; attrtextбез"source": "html"(см.icon-list-item). - Child списка: корневой элемент edit/save —
<li>(семантика списка), не<div>. - Для иконок:
@wp2lms/turbo-icon-picker, не дублировать Lucide в bundle child.
4. PHP: регистрация
Section titled “4. PHP: регистрация”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().
5. Transformer
Section titled “5. Transformer”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 (если блок интерактивный на фронте)
schema.jsonкомпонента:"requiresAlpine": true(для Tabs уже есть в turbo).- Модуль
wp2lms-turbo-blocks/assets/src/frontend/my-block.js+Alpine.data('turboMyBlock', …). - Подключение в
assets/src/entries/turbo.js(import+ вызов register-функции). - В Twig:
x-data="turboMyBlock({ … })"— см.components/Tabs/Tabs.twig. - После правок:
npm run build --prefix wp2lms-turbo-blocks(vite собирает alpine-бандл).
Новый компонент vs переиспользование
| Подход | Когда |
|---|---|
Переиспользовать Tabs / Accordion / … |
Поведение и разметка совпадают с существующим Twig |
| Новый component в turbo + override в Blocks | Нужна другая структура props или a11y-паттерн — правки в onepix/wp2lms/src/Modules/Turbo |
6. Twig + Alpine (Blocks)
Section titled “6. Twig + Alpine (Blocks)”- Базовый 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:
- Стили в
wp2lms-turbo/blocks/src/shared/block-editor.scss. - При необходимости — React-хелпер рядом с
tabs-list.jsx. - Экспорт из
blocks/src/shared/index.js. npm run build --prefix wp2lms-turbo/blocks.
9. Сборка
Section titled “9. Сборка”Из корня monorepo:
npm run build:allИли по шагам:
npm run build:core # shared bundle + turbo pluginnpm run build:skin # blocks package + vite + blocks pluginPulse отдельно:
npm run build:blocks --prefix wp2lms-turbo-blocks # webpack + patch-block-assets.phpphp wp2lms-turbo-blocks/bin/build-plugin.php# Сборка / smoke — через onepix wp2lms (модуль Turbo), не пакет monorepoПорядок важен: сначала core (shared.js), потом skin.
patch-block-assets.php после webpack:
- Добавляет
wp2lms-turbo-blocks-sharedв каждыйindex.asset.php. - Читает
editorScriptиз соседнегоblock.jsonи дописывает named handles (напримерwp2lms-turbo-icon-picker) в dependencies file-скрипта — без этого child может загрузиться раньше icon-picker и упасть сReferenceError.
Без шага 1 блок не загрузит shared bundle.
10. Проверка
Section titled “10. Проверка”| Где | Что проверить |
|---|---|
| Редактор | 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)”Если по таблице выше выбран одиночный вариант:
- Одна папка
blocks/my-block/(block.json+src/index.js). registerTurboBlock+edit/save:- либо статическая разметка + attrs в
InspectorControls; - либо один
InnerBlocksдля произвольного core-контента (безwp2turbo/my-item).
- либо статическая разметка + attrs в
- Один
BlockMapping+ transformer (props из$block['attrs'],$blockContent, опц.$innerBlocks). mapping.jsonне нужен — регистрация программная вRegisterBlocks.php.
Не путать с parent+child: если появляется отдельный тип «элемент» с собственным block.json — переходите на модель Tabs.
Allowlist и категории
Section titled “Allowlist и категории”| Механизм | Поведение |
|---|---|
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-блоки
Section titled “Child-блоки”- Не добавлять child в
EditorBlockRegistry/ inserter filter. "reusable": false— чтобы не выносили из parent.- Mapping на child — только осознанное исключение (card: header/media/button с passthrough).
Категории inserter
Section titled “Категории inserter”- Parent всегда объявляйте с
"category": "wp2lms-turbo"— это fallback, когда reorder выключен. - Фильтр
block_registryдля тематического раздела проверяйте черезSettingsRepository::shouldReorderBlockCategories(). - В фильтр добавляйте только parent, не child. В
RegisterBlocksиспользуйте одинinsertBlockAfter().
Вариации стилей
Section titled “Вариации стилей”- Не храните layout только в локальном state React — он не попадёт на фронт; используйте
className/ attrs блока. - Не забывайте стили вариантов и в block-editor.scss (редактор), и в
assets/src/styles/blocks/(turbo-фронт). - Transformer и Twig должны понимать одни и те же
is-style-*классы.
Сборка
Section titled “Сборка”- Не класть
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.
icon-list / icon-picker — shared layout
Section titled “icon-list / icon-picker — shared layout”| Путь | Назначение |
|---|---|
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в dependencieswp2lms-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.jsoneditorScript — пересобрать blocks (patch-block-assets.phpобновит deps).
Редактор vs фронт
Section titled “Редактор vs фронт”role: localатрибуты не ждать в transformer с сохранённого HTML.- Стили редактора:
block-editor.scss(turbo), не Blocksassets/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.
Хуки для расширения
Section titled “Хуки для расширения”| Хук | Назначение |
|---|---|
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-конфиг редактора |
Связанные документы
Section titled “Связанные документы”| Документ | Содержание |
|---|---|
| обзор 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»”- Скопировать
blocks/tabs→blocks/my-block,blocks/tab-panel→blocks/my-item. - Заменить
wp2turbo/tabs→wp2turbo/my-block, context keys, class names. - Скопировать transformer → переименовать, поправить
blockNameфильтры и props. - Добавить
BlockMappingи при необходимости фильтрblock_registryвRegisterBlocks.php. - Twig + CSS: вариации
is-style-*; при нескольких layout — панель «Стили» вInspectorControls. npm run build:all+ проверка в редакторе и на turbo-странице.
Минимальный diff для «ещё одного блока как icon-list»
Section titled “Минимальный diff для «ещё одного блока как icon-list»”- Скопировать
blocks/icon-list→blocks/my-block,blocks/icon-list-item→blocks/my-item. - Parent:
providesContext, attrs layout; child: attrs элемента + при необходимости@wp2lms/turbo-icon-picker. - Transformer →
items[], layout вынести в resolver при сложной логике (TurboIconLayoutResolverкак образец). BlockMapping→ существующий или новый Twig (ListBlockили свой component).stylesв block.json или InspectorControls — не смешивать без необходимости.- RichText в child: save через
<RichText.Content />, transformer —wp_kses_post(). npm run build:blocks --prefix wp2lms-turbo-blocks+ smoke + редактор.
Минимальный diff для «ещё одного блока как card»
Section titled “Минимальный diff для «ещё одного блока как card»”- Скопировать
blocks/cardи пять child (card-header,card-media,card-content,card-caption,card-button). - Parent: toggles секций,
templateLock: 'all',card-sync.jsдля порядка child. - Child с иконками:
@wp2lms/turbo-icon-picker+ утилиты из@wp2lms/turbo-blocks-shared/icon-layout. card-media— только медиатека (IconMediaPicker), не Lucide.- Mapping: parent →
Card; childcard-header/card-media/card-button— отдельные transformers с passthrough HTML. - Стили карточки:
stylesв block.json +blocks/_shared/card-style/card-style-utils.js. 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).