From 0babd77c9bb36533439be5c8a0a925d1d53d47af Mon Sep 17 00:00:00 2001 From: Yannic van Veen Date: Thu, 3 Sep 2026 09:56:21 +0200 Subject: [PATCH 1/6] feat: init PHP only block functionality --- README.MD | 60 +++++++++++ readme.txt | 2 +- src/PhpBlocks/BladeRenderer.php | 118 ++++++++++++++++++++++ src/PhpBlocks/PhpBlockManager.php | 84 +++++++++++++++ src/PhpBlocks/greeting/block.json | 15 +++ src/PhpBlocks/greeting/greeting.blade.php | 1 + src/PluginServiceProvider.php | 17 ++-- src/Support/AllowedBlocks.php | 37 +++++++ webpack.config.js | 33 +++++- 9 files changed, 355 insertions(+), 12 deletions(-) create mode 100644 src/PhpBlocks/BladeRenderer.php create mode 100644 src/PhpBlocks/PhpBlockManager.php create mode 100644 src/PhpBlocks/greeting/block.json create mode 100644 src/PhpBlocks/greeting/greeting.blade.php create mode 100644 src/Support/AllowedBlocks.php diff --git a/README.MD b/README.MD index 9ccd07f..129cc10 100644 --- a/README.MD +++ b/README.MD @@ -8,6 +8,65 @@ A WordPress plugin with a collection of blocks and features for the Gutenberg ed 2. Run `npm install` to install dependencies 3. Run `npm start` for local development +## 🧱 PHP-only blocks + +Blocks that need no JavaScript live in `src/PhpBlocks/` instead of `src/Blocks/`. They are +registered straight from source and never pass through webpack, so **they need no +`npm run build`** — adding the two files below is enough. + +``` +src/PhpBlocks// +├── block.json +└── .blade.php +``` + +`block.json` must set `supports.autoRegister`. That is what makes WordPress list the block +in the inserter and preview it with core's `ServerSideRender`, without any client-side +registration: + +```json +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "yard/greeting", + "version": "0.1.0", + "title": "Begroeting", + "category": "yard", + "icon": "smiley", + "description": "Toont een begroeting.", + "textdomain": "yard", + "supports": { + "autoRegister": true, + "html": false + } +} +``` + +The Blade template's filename mirrors its directory name and receives these variables: + +| Variable | Description | +| ------------------- | ---------------------------------------------------------------------- | +| `$wrapperAttributes` | Output of `get_block_wrapper_attributes()`. Print it with `{!! !!}`. | +| `$attributes` | The block's attributes, with defaults from `block.json` merged in. | +| `$content` | Inner content, for blocks that have any. | +| `$block` | The `WP_Block` instance. | + +```blade +

Hello World

+``` + +Attributes typed `string`, `number`, `integer`, `boolean` or `enum` get an inspector +control generated for them automatically — no `edit` component to write. + +Notes: + +- **Requires WordPress 7.0+.** On older versions these blocks still render existing + content, they just don't appear in the inserter. +- Blade is rendered through the **host theme's** view engine (Sage/Acorn); this plugin + ships none. See `src/PhpBlocks/BladeRenderer.php`. +- `autoRegister` supports no `InnerBlocks`, media pickers or inline rich text. A block + needing those belongs in `src/Blocks/` with a real `edit` component. + ## 🚀 Release 1. Update plugin versions and run `npm run build` to build assets. Commit and push to remote. @@ -25,6 +84,7 @@ add_filter('yard::gutenberg/allowed-blocks', fn () => [ 'collapse-item', 'counting-number', 'facetwp', + 'greeting', 'icon', 'iconlist', 'iconlist-item', diff --git a/readme.txt b/readme.txt index 6992e75..959a374 100644 --- a/readme.txt +++ b/readme.txt @@ -1,7 +1,7 @@ === Yard | Gutenberg === Contributors: Yard | Digital Agency Tags: block -Tested up to: 6.7 +Tested up to: 7.0 Stable tag: 1.8.0 License: MIT License License URI: https://opensource.org/licenses/MIT diff --git a/src/PhpBlocks/BladeRenderer.php b/src/PhpBlocks/BladeRenderer.php new file mode 100644 index 0000000..afffbfa --- /dev/null +++ b/src/PhpBlocks/BladeRenderer.php @@ -0,0 +1,118 @@ + + */ + private static $reported = []; + + /** + * Render a Blade template. + * + * @param string $templatePath Absolute path to a `.blade.php` file. + * @param array $data Variables made available to the template. + */ + public function render(string $templatePath, array $data = []): string + { + if (! file_exists($templatePath)) { + return $this->fallback($templatePath, sprintf('The Blade template "%s" does not exist.', $templatePath)); + } + + $factory = $this->viewFactory(); + + if (null === $factory) { + return $this->fallback($templatePath, 'No Blade view engine is available. PHP-only blocks are rendered through the view engine of a Sage/Acorn theme.'); + } + + return (string) $factory->file($templatePath, $data); + } + + /** + * Resolve the theme's view factory, or null when there isn't one. + * + * `app()` is Acorn's own global helper, `Roots\app()` its deprecated + * predecessor, and `view()` without arguments returns the factory in both + * Acorn and Laravel. Resolving from the container can throw when the view + * service provider was never registered, hence the try/catch. + * + * @return object|null An `Illuminate\View\Factory`-like object. + */ + private function viewFactory(): ?object + { + foreach (['app', 'Roots\\app'] as $container) { + if (! function_exists($container)) { + continue; + } + + try { + $factory = $container('view'); + } catch (\Throwable $e) { + continue; + } + + if ($this->isViewFactory($factory)) { + return $factory; + } + } + + if (function_exists('view')) { + try { + $factory = \view(); + } catch (\Throwable $e) { + return null; + } + + if ($this->isViewFactory($factory)) { + return $factory; + } + } + + return null; + } + + /** + * @param mixed $factory + */ + private function isViewFactory($factory): bool + { + return is_object($factory) && method_exists($factory, 'file'); + } + + /** + * Handle a template that can't be rendered. + * + * The editor previews these blocks over the REST block-renderer endpoint, so + * there we return a visible hint. On the front end we stay silent: a theme + * without a view engine shouldn't leak diagnostics to visitors. + */ + private function fallback(string $templatePath, string $message): string + { + if (! isset(self::$reported[$templatePath])) { + self::$reported[$templatePath] = true; + + \_doing_it_wrong(__METHOD__, \esc_html($message), '1.8.0'); + } + + if (defined('REST_REQUEST') && REST_REQUEST) { + return '

' . \esc_html__('Dit blok kan niet worden weergegeven: er is geen Blade-templateengine beschikbaar.', 'yard-gutenberg') . '

'; + } + + return ''; + } +} diff --git a/src/PhpBlocks/PhpBlockManager.php b/src/PhpBlocks/PhpBlockManager.php new file mode 100644 index 0000000..53d1073 --- /dev/null +++ b/src/PhpBlocks/PhpBlockManager.php @@ -0,0 +1,84 @@ +renderer = new BladeRenderer(); + } + + public function boot(): void + { + \add_action('init', [$this, 'registerBlocks']); + } + + public function registerBlocks(): void + { + foreach ($this->blockNames() as $blockName) { + $blockPath = __DIR__ . '/' . $blockName; + + \register_block_type($blockPath, [ + 'render_callback' => $this->renderCallback($blockName, $blockPath), + ]); + } + } + + /** + * Every subdirectory holding a `block.json`, minus the ones a site filtered out. + * + * @return string[] + */ + private function blockNames(): array + { + $blockNames = array_map('basename', array_filter(glob(__DIR__ . '/*', GLOB_ONLYDIR) ?: [])); + + $blockNames = array_filter($blockNames, function (string $blockName) { + return file_exists(__DIR__ . '/' . $blockName . '/block.json'); + }); + + return AllowedBlocks::filter($blockNames); + } + + /** + * Build the render callback for a single block. + * + * The template lives next to the block's `block.json` and mirrors its + * directory name, e.g. `greeting/greeting.blade.php`. + * + * `get_block_wrapper_attributes()` reads the block currently being rendered, + * so it has to be called inside the callback rather than up front. + */ + private function renderCallback(string $blockName, string $blockPath): callable + { + $templatePath = $blockPath . '/' . $blockName . '.blade.php'; + + return function ($attributes, $content = '', $block = null) use ($templatePath) { + return $this->renderer->render($templatePath, [ + 'attributes' => is_array($attributes) ? $attributes : [], + 'content' => $content, + 'block' => $block, + 'wrapperAttributes' => \get_block_wrapper_attributes(), + ]); + }; + } +} diff --git a/src/PhpBlocks/greeting/block.json b/src/PhpBlocks/greeting/block.json new file mode 100644 index 0000000..4b5420d --- /dev/null +++ b/src/PhpBlocks/greeting/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "yard/greeting", + "version": "0.1.0", + "title": "Begroeting", + "category": "yard", + "icon": "smiley", + "description": "Toont een begroeting.", + "textdomain": "yard", + "supports": { + "autoRegister": true, + "html": false + } +} diff --git a/src/PhpBlocks/greeting/greeting.blade.php b/src/PhpBlocks/greeting/greeting.blade.php new file mode 100644 index 0000000..a417c77 --- /dev/null +++ b/src/PhpBlocks/greeting/greeting.blade.php @@ -0,0 +1 @@ +

Hello World

diff --git a/src/PluginServiceProvider.php b/src/PluginServiceProvider.php index 0c7fab4..b627630 100644 --- a/src/PluginServiceProvider.php +++ b/src/PluginServiceProvider.php @@ -4,6 +4,8 @@ namespace Yard\Gutenberg; +use Yard\Gutenberg\Support\AllowedBlocks; + class PluginServiceProvider { public function boot() @@ -35,6 +37,7 @@ public function bootProviders(): void Hooks\DefaultHookManager::class, MyPatterns\MyPatternManager::class, Patterns\PatternManager::class, + PhpBlocks\PhpBlockManager::class, YardPatterns\YardPatternsManager::class, ]; @@ -89,16 +92,12 @@ public function registerBlocks() $blockNames = array_map('basename', array_filter(glob($blocksPath . '*', GLOB_ONLYDIR) ?: [])); - if (has_filter('yard::gutenberg/allowed-blocks')) { - $allowedBlocks = apply_filters('yard::gutenberg/allowed-blocks', []); + // Skip leftover directories from an earlier build that no longer hold a block. + $blockNames = array_filter($blockNames, function (string $blockName) use ($blocksPath) { + return file_exists($blocksPath . $blockName . '/block.json'); + }); - $blockNames = array_filter( - $blockNames, - function (string $blockName) use ($allowedBlocks) { - return in_array($blockName, $allowedBlocks); - } - ); - } + $blockNames = AllowedBlocks::filter($blockNames); foreach ($blockNames as $blockName) { $blockPath = $blocksPath . $blockName; diff --git a/src/Support/AllowedBlocks.php b/src/Support/AllowedBlocks.php new file mode 100644 index 0000000..648c2ee --- /dev/null +++ b/src/Support/AllowedBlocks.php @@ -0,0 +1,37 @@ + { + if ( ! Array.isArray( plugin?.patterns ) ) { + return; + } + + plugin.patterns = plugin.patterns.map( ( pattern ) => ( { + ...pattern, + globOptions: { + ...pattern.globOptions, + ignore: [ + ...( pattern.globOptions?.ignore ?? [] ), + '**/PhpBlocks/**', + ], + }, + } ) ); +} ); + +module.exports = config; From b859275771e7cbc9332505207bf45b3855810812 Mon Sep 17 00:00:00 2001 From: Yannic van Veen Date: Thu, 3 Sep 2026 11:10:34 +0200 Subject: [PATCH 2/6] feat: add viewmodel and greeting logic --- README.MD | 89 ++++++++++++--- src/PhpBlocks/BlockViewModel.php | 106 ++++++++++++++++++ src/PhpBlocks/Greeting/Greeting.php | 79 +++++++++++++ .../{greeting => Greeting}/block.json | 0 src/PhpBlocks/Greeting/greeting.blade.php | 1 + src/PhpBlocks/PhpBlockManager.php | 95 ++++++++++++---- src/PhpBlocks/greeting/greeting.blade.php | 1 - src/Support/AllowedBlocks.php | 9 +- 8 files changed, 339 insertions(+), 41 deletions(-) create mode 100644 src/PhpBlocks/BlockViewModel.php create mode 100644 src/PhpBlocks/Greeting/Greeting.php rename src/PhpBlocks/{greeting => Greeting}/block.json (100%) create mode 100644 src/PhpBlocks/Greeting/greeting.blade.php delete mode 100644 src/PhpBlocks/greeting/greeting.blade.php diff --git a/README.MD b/README.MD index 129cc10..6021742 100644 --- a/README.MD +++ b/README.MD @@ -12,14 +12,21 @@ A WordPress plugin with a collection of blocks and features for the Gutenberg ed Blocks that need no JavaScript live in `src/PhpBlocks/` instead of `src/Blocks/`. They are registered straight from source and never pass through webpack, so **they need no -`npm run build`** — adding the two files below is enough. +`npm run build`** — adding the files below is enough. ``` -src/PhpBlocks// -├── block.json -└── .blade.php +src/PhpBlocks/Greeting/ +├── block.json "name": "yard/greeting" +├── greeting.blade.php named after the slug in block.json +└── Greeting.php optional; the block's logic ``` +The folder is PascalCase because it doubles as a PHP namespace segment. The block's +identity comes from `block.json`'s `name`, not from the folder: the part after the `/` is +the slug, which names the Blade template and is the key a site filters on through +[`yard::gutenberg/allowed-blocks`](#yardgutenbergallowed-blocks). So a folder `OpeningHours` +with `"name": "yard/opening-hours"` looks for `opening-hours.blade.php`. + `block.json` must set `supports.autoRegister`. That is what makes WordPress list the block in the inserter and preview it with core's `ServerSideRender`, without any client-side registration: @@ -42,23 +49,59 @@ registration: } ``` -The Blade template's filename mirrors its directory name and receives these variables: +Every template receives these variables: -| Variable | Description | -| ------------------- | ---------------------------------------------------------------------- | -| `$wrapperAttributes` | Output of `get_block_wrapper_attributes()`. Print it with `{!! !!}`. | -| `$attributes` | The block's attributes, with defaults from `block.json` merged in. | -| `$content` | Inner content, for blocks that have any. | -| `$block` | The `WP_Block` instance. | +| Variable | Description | +| -------------------- | -------------------------------------------------------------------- | +| `$wrapperAttributes` | Output of `get_block_wrapper_attributes()`. Print it with `{!! !!}`. | +| `$attributes` | The block's attributes, with the defaults from `block.json` merged. | +| `$content` | Inner content, for blocks that have any. | +| `$block` | The `WP_Block` instance. | ```blade -

Hello World

+

{{ $greeting }}

``` Attributes typed `string`, `number`, `integer`, `boolean` or `enum` get an inspector control generated for them automatically — no `edit` component to write. -Notes: +### View models + +To keep logic out of the template, add a class in the block's folder named after the +folder, extending `BlockViewModel`. It is picked up automatically; a block without one +renders on the four variables above alone. + +```php +namespace Yard\Gutenberg\PhpBlocks\Greeting; + +use Yard\Gutenberg\PhpBlocks\BlockViewModel; + +class Greeting extends BlockViewModel +{ + public function with(): array + { + return ['greeting' => $this->greeting()]; + } + + private function greeting(): string + { + // $this->attributes(), $this->attribute('name', $default), + // $this->content(), $this->block(), $this->wrapperAttributes() + } +} +``` + +`with()` supplies data, `override()` supplies data that wins over everything, and the +merge order is `with()` → the four variables above → `override()`. That is deliberately +the same shape and precedence as `Roots\Acorn\View\Composer`, so the class reads like the +composers in a Sage theme. + +An actual Acorn view composer cannot serve these blocks, so don't reach for one: composers +are matched on the *view name*, and rendering a template by absolute path through +`Illuminate\View\Factory::file()` uses that path as the view name. A composer registered +for `blocks.greeting` never fires. + +### Notes - **Requires WordPress 7.0+.** On older versions these blocks still render existing content, they just don't appear in the inserter. @@ -66,6 +109,9 @@ Notes: ships none. See `src/PhpBlocks/BladeRenderer.php`. - `autoRegister` supports no `InnerBlocks`, media pickers or inline rich text. A block needing those belongs in `src/Blocks/` with a real `edit` component. +- Anything time- or request-dependent (such as `yard/greeting`) is frozen by a full-page + cache at whatever value the page was cached with. A site that needs it accurate has to + exclude the page from caching or render that part client-side. ## 🚀 Release @@ -119,6 +165,23 @@ public function registerCoreBlocks($initialAllowedBlocks): array } ``` +### `yard::gutenberg/greeting-periods` + +Change the greeting the `yard/greeting` block shows, and when. The map is keyed by the hour +a period starts (in the site's timezone); any number of periods works and the day wraps, so +a map starting at 6 leaves the small hours to the last period. + +```PHP +add_filter('yard::gutenberg/greeting-periods', fn () => [ + 6 => 'Goedemorgen,', + 12 => 'Goedemiddag,', + 18 => 'Goedenavond,', +]); +``` + +The defaults are `0 => 'Goedenacht'`, `6 => 'Goedemorgen'`, `12 => 'Goedemiddag'`, +`18 => 'Goedenavond'`. + ### `yard::gutenberg/allowed-blocks-whitelisted-prefixes` By default, all blocks are registered. Use this filter to register only the allowed blocks with a specific prefix. The example adds the `tribe` prefix: diff --git a/src/PhpBlocks/BlockViewModel.php b/src/PhpBlocks/BlockViewModel.php new file mode 100644 index 0000000..cb1f0af --- /dev/null +++ b/src/PhpBlocks/BlockViewModel.php @@ -0,0 +1,106 @@ + + */ + private $data = []; + + /** + * Merge this view model into the block's render data. + * + * Following Acorn: `with()` supplies defaults, the block's own render data + * beats them, and `override()` beats everything — which is how a block + * replaces something it is given, such as `wrapperAttributes`. + * + * @param array $data + * + * @return array + */ + final public function compose(array $data): array + { + $this->data = $data; + + return array_merge($this->with(), $data, $this->override()); + } + + /** + * Data passed to the Blade template. + * + * @return array + */ + public function with(): array + { + return []; + } + + /** + * Data passed to the Blade template, winning over everything else. + * + * @return array + */ + public function override(): array + { + return []; + } + + /** + * The block's attributes, with the defaults from `block.json` already merged + * in by `WP_Block_Type::prepare_attributes_for_render()`. + * + * @return array + */ + protected function attributes(): array + { + return is_array($this->data['attributes'] ?? null) ? $this->data['attributes'] : []; + } + + /** + * @param mixed $default + * + * @return mixed + */ + protected function attribute(string $name, $default = null) + { + $attributes = $this->attributes(); + + return array_key_exists($name, $attributes) ? $attributes[$name] : $default; + } + + protected function content(): string + { + return is_string($this->data['content'] ?? null) ? $this->data['content'] : ''; + } + + protected function block(): ?\WP_Block + { + return ($this->data['block'] ?? null) instanceof \WP_Block ? $this->data['block'] : null; + } + + protected function wrapperAttributes(): string + { + return is_string($this->data['wrapperAttributes'] ?? null) ? $this->data['wrapperAttributes'] : ''; + } +} diff --git a/src/PhpBlocks/Greeting/Greeting.php b/src/PhpBlocks/Greeting/Greeting.php new file mode 100644 index 0000000..3ae7633 --- /dev/null +++ b/src/PhpBlocks/Greeting/Greeting.php @@ -0,0 +1,79 @@ + $this->greeting(), + ]; + } + + /** + * The greeting for the current hour in the site's timezone. + * + * `current_datetime()` is already in `wp_timezone()`, so this follows the + * site's clock rather than the server's. + */ + private function greeting(): string + { + $periods = $this->periods(); + + if ([] === $periods) { + return ''; + } + + $hour = (int) \current_datetime()->format('G'); + + // The day wraps: hours before the first period belong to the last one. + $greeting = end($periods); + + foreach ($periods as $startHour => $text) { + if ((int) $startHour <= $hour) { + $greeting = $text; + } + } + + return (string) $greeting; + } + + /** + * The greeting per period, keyed by the hour the period starts, sorted. + * + * A site may return any number of periods and need not start at hour 0 — + * `greeting()` wraps around, so a map starting at 6 leaves the small hours + * to the final period. + * + * @return array Start hour (0-23) => greeting. + */ + private function periods(): array + { + /** + * Filter the greeting shown per period. + * + * @param array $periods Start hour (0-23) => greeting. + */ + $periods = \apply_filters('yard::gutenberg/greeting-periods', [ + 0 => \__('Goedenacht', 'yard-gutenberg'), + 6 => \__('Goedemorgen', 'yard-gutenberg'), + 12 => \__('Goedemiddag', 'yard-gutenberg'), + 18 => \__('Goedenavond', 'yard-gutenberg'), + ]); + + if (! is_array($periods)) { + return []; + } + + $periods = array_filter($periods, 'is_scalar'); + + ksort($periods, SORT_NUMERIC); + + return $periods; + } +} diff --git a/src/PhpBlocks/greeting/block.json b/src/PhpBlocks/Greeting/block.json similarity index 100% rename from src/PhpBlocks/greeting/block.json rename to src/PhpBlocks/Greeting/block.json diff --git a/src/PhpBlocks/Greeting/greeting.blade.php b/src/PhpBlocks/Greeting/greeting.blade.php new file mode 100644 index 0000000..41a4737 --- /dev/null +++ b/src/PhpBlocks/Greeting/greeting.blade.php @@ -0,0 +1 @@ +

{{ $greeting }}

diff --git a/src/PhpBlocks/PhpBlockManager.php b/src/PhpBlocks/PhpBlockManager.php index 53d1073..006a58c 100644 --- a/src/PhpBlocks/PhpBlockManager.php +++ b/src/PhpBlocks/PhpBlockManager.php @@ -9,11 +9,18 @@ /** * Registers the PHP-only blocks in `src/PhpBlocks`. * - * These blocks have no JavaScript and never pass through webpack: each one is a - * `block.json` plus a Blade template of the same name, registered straight from - * source. WordPress 7.0's `supports.autoRegister` puts them in the inserter and - * previews them with core's `ServerSideRender`, so no client-side registration - * is needed. + * These blocks have no JavaScript and never pass through webpack. A block is a + * PascalCase folder holding a `block.json`, a Blade template named after the + * block's slug, and optionally a `BlockViewModel` of the same name as the + * folder: + * + * Greeting/ + * ├── block.json "name": "yard/greeting" + * ├── greeting.blade.php + * └── Greeting.php extends BlockViewModel + * + * WordPress 7.0's `supports.autoRegister` puts them in the inserter and previews + * them with core's `ServerSideRender`, so no client-side registration is needed. * * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#autoregister */ @@ -34,51 +41,91 @@ public function boot(): void public function registerBlocks(): void { - foreach ($this->blockNames() as $blockName) { - $blockPath = __DIR__ . '/' . $blockName; + foreach ($this->blocks() as $directory => $slug) { + $blockPath = __DIR__ . '/' . $directory; \register_block_type($blockPath, [ - 'render_callback' => $this->renderCallback($blockName, $blockPath), + 'render_callback' => $this->renderCallback($directory, $slug, $blockPath), ]); } } /** - * Every subdirectory holding a `block.json`, minus the ones a site filtered out. + * The registrable blocks, as folder name => block slug. + * + * The slug comes from `block.json`, not from the folder: the folder has to be + * a valid namespace segment for the view model to autoload, while the slug is + * the block's actual identity — it names the template and is the key a site + * filters on through `yard::gutenberg/allowed-blocks`, where hyphenated names + * like `opening-hours` are perfectly normal. * - * @return string[] + * @return array */ - private function blockNames(): array + private function blocks(): array { - $blockNames = array_map('basename', array_filter(glob(__DIR__ . '/*', GLOB_ONLYDIR) ?: [])); + $blocks = []; - $blockNames = array_filter($blockNames, function (string $blockName) { - return file_exists(__DIR__ . '/' . $blockName . '/block.json'); - }); + foreach (array_filter(glob(__DIR__ . '/*', GLOB_ONLYDIR) ?: []) as $path) { + $slug = $this->slug($path . '/block.json'); - return AllowedBlocks::filter($blockNames); + if (null !== $slug) { + $blocks[basename($path)] = $slug; + } + } + + return AllowedBlocks::filter($blocks); } /** - * Build the render callback for a single block. + * The slug from a `block.json`, e.g. `greeting` for `yard/greeting`. * - * The template lives next to the block's `block.json` and mirrors its - * directory name, e.g. `greeting/greeting.blade.php`. + * Returns null for a folder that holds no readable metadata with a namespaced + * block name, so a stray directory is skipped rather than fatal. + */ + private function slug(string $metadataPath): ?string + { + if (! file_exists($metadataPath)) { + return null; + } + + $metadata = json_decode((string) file_get_contents($metadataPath), true); + $name = is_array($metadata) && is_string($metadata['name'] ?? null) ? $metadata['name'] : ''; + $separator = strpos($name, '/'); + + if (false === $separator) { + return null; + } + + $slug = substr($name, $separator + 1); + + return '' === $slug ? null : $slug; + } + + /** + * Build the render callback for a single block. * * `get_block_wrapper_attributes()` reads the block currently being rendered, * so it has to be called inside the callback rather than up front. */ - private function renderCallback(string $blockName, string $blockPath): callable + private function renderCallback(string $directory, string $slug, string $blockPath): callable { - $templatePath = $blockPath . '/' . $blockName . '.blade.php'; + $templatePath = $blockPath . '/' . $slug . '.blade.php'; + $viewModelClass = __NAMESPACE__ . '\\' . $directory . '\\' . $directory; - return function ($attributes, $content = '', $block = null) use ($templatePath) { - return $this->renderer->render($templatePath, [ + return function ($attributes, $content = '', $block = null) use ($templatePath, $viewModelClass) { + $data = [ 'attributes' => is_array($attributes) ? $attributes : [], 'content' => $content, 'block' => $block, 'wrapperAttributes' => \get_block_wrapper_attributes(), - ]); + ]; + + // Blocks without a view model render on this data alone. + if (is_subclass_of($viewModelClass, BlockViewModel::class)) { + $data = (new $viewModelClass())->compose($data); + } + + return $this->renderer->render($templatePath, $data); }; } } diff --git a/src/PhpBlocks/greeting/greeting.blade.php b/src/PhpBlocks/greeting/greeting.blade.php deleted file mode 100644 index a417c77..0000000 --- a/src/PhpBlocks/greeting/greeting.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Hello World

diff --git a/src/Support/AllowedBlocks.php b/src/Support/AllowedBlocks.php index 648c2ee..5ac4ef8 100644 --- a/src/Support/AllowedBlocks.php +++ b/src/Support/AllowedBlocks.php @@ -7,7 +7,7 @@ class AllowedBlocks { /** - * Filter a list of block directory names through the + * Filter a list of block names through the * `yard::gutenberg/allowed-blocks` filter. * * By default every block this plugin ships is registered. As soon as a site @@ -15,9 +15,12 @@ class AllowedBlocks * in `build/Blocks` and the PHP-only blocks in `src/PhpBlocks` go through * here, so one filter controls every block in the plugin. * - * @param string[] $blockNames Block directory names. + * Filtering is on the values and keys are preserved, so callers may pass + * either a plain list of names or a map of something else onto them. * - * @return string[] The allowed block directory names. + * @param string[] $blockNames Block names. + * + * @return string[] The allowed block names. */ public static function filter(array $blockNames): array { From 64f3e31f6588be916d3d0d07ea0b6fd4fb28c476 Mon Sep 17 00:00:00 2001 From: Yannic van Veen Date: Thu, 3 Sep 2026 11:53:20 +0200 Subject: [PATCH 3/6] feat: add suffix attribute and BEM class logic --- README.MD | 76 ++++++++++++--- src/PhpBlocks/BlockViewModel.php | 96 ++++++++++++++---- src/PhpBlocks/Greeting/Greeting.php | 85 +++++++++++----- src/PhpBlocks/Greeting/block.json | 7 ++ src/PhpBlocks/Greeting/greeting.blade.php | 9 +- src/PhpBlocks/PhpBlockManager.php | 113 ++++++++++++++-------- 6 files changed, 290 insertions(+), 96 deletions(-) diff --git a/README.MD b/README.MD index 6021742..9c8da00 100644 --- a/README.MD +++ b/README.MD @@ -57,9 +57,12 @@ Every template receives these variables: | `$attributes` | The block's attributes, with the defaults from `block.json` merged. | | `$content` | Inner content, for blocks that have any. | | `$block` | The `WP_Block` instance. | +| `$blockClass` | The block's generated class, e.g. `wp-block-yard-greeting`. | ```blade -

{{ $greeting }}

+

+ {{ $greeting }} +

``` Attributes typed `string`, `number`, `integer`, `boolean` or `enum` get an inspector @@ -68,8 +71,8 @@ control generated for them automatically — no `edit` component to write. ### View models To keep logic out of the template, add a class in the block's folder named after the -folder, extending `BlockViewModel`. It is picked up automatically; a block without one -renders on the four variables above alone. +folder, extending `BlockViewModel`. It is picked up automatically; a block without one is +rendered by a plain `BlockViewModel`, which adds nothing. ```php namespace Yard\Gutenberg\PhpBlocks\Greeting; @@ -83,24 +86,63 @@ class Greeting extends BlockViewModel return ['greeting' => $this->greeting()]; } + public function classes(): array + { + return [$this->modifier('morning')]; + } + private function greeting(): string { // $this->attributes(), $this->attribute('name', $default), - // $this->content(), $this->block(), $this->wrapperAttributes() + // $this->content(), $this->block(), $this->blockClass() } } ``` -`with()` supplies data, `override()` supplies data that wins over everything, and the -merge order is `with()` → the four variables above → `override()`. That is deliberately -the same shape and precedence as `Roots\Acorn\View\Composer`, so the class reads like the -composers in a Sage theme. +| Method | Purpose | +| ------------ | ------------------------------------------------------------------------------ | +| `with()` | Data for the template. | +| `override()` | Data for the template that wins over everything else. | +| `classes()` | Extra classes for the wrapper element. | + +The merge order is `with()` → the variables in the table above → `override()`. That is +deliberately the same shape and precedence as `Roots\Acorn\View\Composer`, so the class +reads like the composers in a Sage theme. + +`modifier('morning')` returns `wp-block-yard-greeting--morning`. Build modifiers with it +rather than writing the base class by hand: it derives from `$blockClass`, so it follows +the `block_default_classname` filter, and it runs the slug through +`sanitize_html_class()`. An actual Acorn view composer cannot serve these blocks, so don't reach for one: composers are matched on the *view name*, and rendering a template by absolute path through `Illuminate\View\Factory::file()` uses that path as the view name. A composer registered for `blocks.greeting` never fires. +### Class names + +`wp-block-yard-` is generated by WordPress, not by this plugin — +`wp_get_block_default_classname()` turns `yard/greeting` into `wp-block-yard-greeting`, +and the `generated-classname` block support puts it on the wrapper. It is the BEM block +that `src/Blocks/*/style.scss` already builds on: + +```scss +$block: 'wp-block-yard-greeting'; + +.#{$block} { + &--morning { /* from classes() */ } + &__text { /* from the template */ } +} +``` + +- Classes from `classes()` are **merged ahead** of the generated one by + `get_block_wrapper_attributes()`, and de-duplicated. The same call also concatenates + `style`, lets an explicit `id` or `aria-label` win, and passes any other attribute + through untouched. +- `"supports": { "className": false }` in a `block.json` drops the generated class. +- The `block_default_classname` filter renames it globally; `$blockClass` and every + `modifier()` follow along. + ### Notes - **Requires WordPress 7.0+.** On older versions these blocks still render existing @@ -167,20 +209,22 @@ public function registerCoreBlocks($initialAllowedBlocks): array ### `yard::gutenberg/greeting-periods` -Change the greeting the `yard/greeting` block shows, and when. The map is keyed by the hour -a period starts (in the site's timezone); any number of periods works and the day wraps, so -a map starting at 6 leaves the small hours to the last period. +Change the greeting the `yard/greeting` block shows, and when. The array key is the BEM +modifier put on the block's wrapper (`morning` renders +`wp-block-yard-greeting--morning`), `from` is the hour the period starts in the site's +timezone, and `greeting` is the text. ```PHP add_filter('yard::gutenberg/greeting-periods', fn () => [ - 6 => 'Goedemorgen,', - 12 => 'Goedemiddag,', - 18 => 'Goedenavond,', + 'morning' => ['from' => 6, 'greeting' => 'Goedemorgen,'], + 'afternoon' => ['from' => 12, 'greeting' => 'Goedemiddag,'], + 'evening' => ['from' => 18, 'greeting' => 'Goedenavond,'], ]); ``` -The defaults are `0 => 'Goedenacht'`, `6 => 'Goedemorgen'`, `12 => 'Goedemiddag'`, -`18 => 'Goedenavond'`. +Any number of periods works and the map need not start at hour 0 — the day wraps, so the +example above leaves the small hours to `evening`. The defaults are `night` from 0, +`morning` from 6, `afternoon` from 12 and `evening` from 18. ### `yard::gutenberg/allowed-blocks-whitelisted-prefixes` diff --git a/src/PhpBlocks/BlockViewModel.php b/src/PhpBlocks/BlockViewModel.php index cb1f0af..2ad8320 100644 --- a/src/PhpBlocks/BlockViewModel.php +++ b/src/PhpBlocks/BlockViewModel.php @@ -8,9 +8,10 @@ * Holds the logic behind a single PHP-only block. * * A block folder may contain a class of the same name extending this one, e.g. - * `Greeting/Greeting.php`. `PhpBlockManager` finds it, hands it the data it was - * going to pass to the Blade template, and passes on whatever comes back — so - * domain logic lives in a testable class instead of an `@php` block. + * `Greeting/Greeting.php`. `PhpBlockManager` finds it and lets it assemble the + * data the Blade template is rendered with, so domain logic lives in a testable + * class instead of an `@php` block. A block without one is rendered by a plain + * instance of this class, which adds nothing. * * The `with()` / `override()` pair and their merge order are deliberately * identical to `Roots\Acorn\View\Composer`, which the Sage themes around this @@ -19,29 +20,58 @@ * uses the template's absolute path as that name, so a composer registered for * a dotted view never fires for a block rendered by path. */ -abstract class BlockViewModel +class BlockViewModel { + /** @var array */ + private $attributes = []; + + /** @var string */ + private $content = ''; + + /** @var \WP_Block|null */ + private $block; + /** - * The data the block was going to be rendered with. + * The block's generated class, e.g. `wp-block-yard-greeting`. * - * @var array + * @var string */ - private $data = []; + private $blockClass = ''; + + /** @var string */ + private $wrapperAttributes = ''; /** - * Merge this view model into the block's render data. + * Assemble the data the Blade template is rendered with. * * Following Acorn: `with()` supplies defaults, the block's own render data * beats them, and `override()` beats everything — which is how a block * replaces something it is given, such as `wrapperAttributes`. * - * @param array $data + * @param array $attributes Attributes, with `block.json` defaults merged in. * * @return array */ - final public function compose(array $data): array + final public function compose(array $attributes, string $content, ?\WP_Block $block, string $blockClass): array { - $this->data = $data; + $this->attributes = $attributes; + $this->content = $content; + $this->block = $block; + $this->blockClass = $blockClass; + + // An empty class is harmless: core splits it with PREG_SPLIT_NO_EMPTY, + // so it contributes nothing to the merge. + $this->wrapperAttributes = \get_block_wrapper_attributes([ + 'class' => implode(' ', array_filter($this->classes())), + ]); + + $data = [ + 'attributes' => $attributes, + 'content' => $content, + 'block' => $block, + 'blockClass' => $blockClass, + 'wrapperAttributes' => $this->wrapperAttributes, + ]; return array_merge($this->with(), $data, $this->override()); } @@ -66,6 +96,33 @@ public function override(): array return []; } + /** + * Extra classes for the block's wrapper element. + * + * `get_block_wrapper_attributes()` merges these ahead of the class + * WordPress generates, and de-duplicates. + * + * @return string[] + */ + public function classes(): array + { + return []; + } + + /** + * A BEM modifier on the block's generated class. + * + * Built from `$blockClass` rather than a literal so it follows the + * `block_default_classname` filter, and matches the `$block` variable the + * block's SCSS uses. + */ + protected function modifier(string $name): string + { + $name = \sanitize_html_class($name); + + return '' === $this->blockClass || '' === $name ? '' : $this->blockClass . '--' . $name; + } + /** * The block's attributes, with the defaults from `block.json` already merged * in by `WP_Block_Type::prepare_attributes_for_render()`. @@ -74,7 +131,7 @@ public function override(): array */ protected function attributes(): array { - return is_array($this->data['attributes'] ?? null) ? $this->data['attributes'] : []; + return $this->attributes; } /** @@ -84,23 +141,26 @@ protected function attributes(): array */ protected function attribute(string $name, $default = null) { - $attributes = $this->attributes(); - - return array_key_exists($name, $attributes) ? $attributes[$name] : $default; + return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } protected function content(): string { - return is_string($this->data['content'] ?? null) ? $this->data['content'] : ''; + return $this->content; } protected function block(): ?\WP_Block { - return ($this->data['block'] ?? null) instanceof \WP_Block ? $this->data['block'] : null; + return $this->block; + } + + protected function blockClass(): string + { + return $this->blockClass; } protected function wrapperAttributes(): string { - return is_string($this->data['wrapperAttributes'] ?? null) ? $this->data['wrapperAttributes'] : ''; + return $this->wrapperAttributes; } } diff --git a/src/PhpBlocks/Greeting/Greeting.php b/src/PhpBlocks/Greeting/Greeting.php index 3ae7633..8b79952 100644 --- a/src/PhpBlocks/Greeting/Greeting.php +++ b/src/PhpBlocks/Greeting/Greeting.php @@ -8,72 +8,111 @@ class Greeting extends BlockViewModel { + /** + * The resolved period, memoised: `with()` and `classes()` both need it, and + * resolving twice could straddle an hour boundary. + * + * @var array{slug: string, greeting: string}|null + */ + private $period; + public function with(): array { return [ - 'greeting' => $this->greeting(), + 'greeting' => $this->period()['greeting'], ]; } + public function classes(): array + { + return array_filter([$this->modifier($this->period()['slug'])]); + } + /** - * The greeting for the current hour in the site's timezone. + * The period covering the current hour in the site's timezone. * * `current_datetime()` is already in `wp_timezone()`, so this follows the * site's clock rather than the server's. + * + * @return array{slug: string, greeting: string} */ - private function greeting(): string + private function period(): array { + if (null !== $this->period) { + return $this->period; + } + $periods = $this->periods(); if ([] === $periods) { - return ''; + return $this->period = ['slug' => '', 'greeting' => '']; } $hour = (int) \current_datetime()->format('G'); // The day wraps: hours before the first period belong to the last one. - $greeting = end($periods); + $slugs = array_keys($periods); + $slug = end($slugs); - foreach ($periods as $startHour => $text) { - if ((int) $startHour <= $hour) { - $greeting = $text; + foreach ($periods as $candidate => $period) { + if ($period['from'] <= $hour) { + $slug = $candidate; } } - return (string) $greeting; + return $this->period = [ + 'slug' => (string) $slug, + 'greeting' => $periods[$slug]['greeting'], + ]; } /** - * The greeting per period, keyed by the hour the period starts, sorted. + * The greeting periods, keyed by slug and sorted by start hour. * - * A site may return any number of periods and need not start at hour 0 — - * `greeting()` wraps around, so a map starting at 6 leaves the small hours - * to the final period. + * The key doubles as the BEM modifier on the block's class. A site may return + * any number of periods and need not start at hour 0 — `period()` wraps + * around, so a map starting at 6 leaves the small hours to the final period. * - * @return array Start hour (0-23) => greeting. + * @return array */ private function periods(): array { /** - * Filter the greeting shown per period. + * Filter the greeting periods. + * + * Keyed by slug, which becomes the BEM modifier on the block's class, + * e.g. `morning` renders `wp-block-yard-greeting--morning`. * - * @param array $periods Start hour (0-23) => greeting. + * @param array $periods */ $periods = \apply_filters('yard::gutenberg/greeting-periods', [ - 0 => \__('Goedenacht', 'yard-gutenberg'), - 6 => \__('Goedemorgen', 'yard-gutenberg'), - 12 => \__('Goedemiddag', 'yard-gutenberg'), - 18 => \__('Goedenavond', 'yard-gutenberg'), + 'night' => ['from' => 0, 'greeting' => \__('Goedenacht', 'yard-gutenberg')], + 'morning' => ['from' => 6, 'greeting' => \__('Goedemorgen', 'yard-gutenberg')], + 'afternoon' => ['from' => 12, 'greeting' => \__('Goedemiddag', 'yard-gutenberg')], + 'evening' => ['from' => 18, 'greeting' => \__('Goedenavond', 'yard-gutenberg')], ]); if (! is_array($periods)) { return []; } - $periods = array_filter($periods, 'is_scalar'); + $normalized = []; + + foreach ($periods as $slug => $period) { + if (! is_array($period) || ! isset($period['from']) || ! isset($period['greeting'])) { + continue; + } + + $normalized[(string) $slug] = [ + 'from' => (int) $period['from'], + 'greeting' => (string) $period['greeting'], + ]; + } - ksort($periods, SORT_NUMERIC); + uasort($normalized, function (array $a, array $b) { + return $a['from'] <=> $b['from']; + }); - return $periods; + return $normalized; } } diff --git a/src/PhpBlocks/Greeting/block.json b/src/PhpBlocks/Greeting/block.json index 4b5420d..6da3928 100644 --- a/src/PhpBlocks/Greeting/block.json +++ b/src/PhpBlocks/Greeting/block.json @@ -11,5 +11,12 @@ "supports": { "autoRegister": true, "html": false + }, + "attributes": { + "suffix": { + "label": "Tekst na de begroeting", + "type": "string", + "default": "" + } } } diff --git a/src/PhpBlocks/Greeting/greeting.blade.php b/src/PhpBlocks/Greeting/greeting.blade.php index 41a4737..ffc8ff6 100644 --- a/src/PhpBlocks/Greeting/greeting.blade.php +++ b/src/PhpBlocks/Greeting/greeting.blade.php @@ -1 +1,8 @@ -

{{ $greeting }}

+

+ @if (filled($attributes['suffix'])) + {{ $greeting }}, + {{ $attributes['suffix'] }} + @else + {{ $greeting }} + @endif +

diff --git a/src/PhpBlocks/PhpBlockManager.php b/src/PhpBlocks/PhpBlockManager.php index 006a58c..fb906dc 100644 --- a/src/PhpBlocks/PhpBlockManager.php +++ b/src/PhpBlocks/PhpBlockManager.php @@ -41,23 +41,23 @@ public function boot(): void public function registerBlocks(): void { - foreach ($this->blocks() as $directory => $slug) { + foreach ($this->blocks() as $directory => $blockName) { $blockPath = __DIR__ . '/' . $directory; \register_block_type($blockPath, [ - 'render_callback' => $this->renderCallback($directory, $slug, $blockPath), + 'render_callback' => $this->renderCallback($directory, $blockName, $blockPath), ]); } } /** - * The registrable blocks, as folder name => block slug. + * The registrable blocks, as folder name => block name. * - * The slug comes from `block.json`, not from the folder: the folder has to be - * a valid namespace segment for the view model to autoload, while the slug is - * the block's actual identity — it names the template and is the key a site - * filters on through `yard::gutenberg/allowed-blocks`, where hyphenated names - * like `opening-hours` are perfectly normal. + * The name comes from `block.json`, not from the folder: the folder has to be + * a valid namespace segment for the view model to autoload, while the name is + * the block's actual identity. Its slug names the template and is the key a + * site filters on through `yard::gutenberg/allowed-blocks`, where hyphenated + * names like `opening-hours` are perfectly normal. * * @return array */ @@ -66,23 +66,40 @@ private function blocks(): array $blocks = []; foreach (array_filter(glob(__DIR__ . '/*', GLOB_ONLYDIR) ?: []) as $path) { - $slug = $this->slug($path . '/block.json'); + $blockName = $this->blockName($path . '/block.json'); - if (null !== $slug) { - $blocks[basename($path)] = $slug; + if (null !== $blockName) { + $blocks[basename($path)] = $blockName; } } - return AllowedBlocks::filter($blocks); + return $this->filterAllowed($blocks); } /** - * The slug from a `block.json`, e.g. `greeting` for `yard/greeting`. + * Apply `yard::gutenberg/allowed-blocks`, which is keyed on the slug. + * + * `AllowedBlocks::filter()` tests values and preserves keys, so it filters a + * folder => slug map, and the surviving keys select from the original. + * + * @param array $blocks Folder name => block name. + * + * @return array + */ + private function filterAllowed(array $blocks): array + { + $allowed = AllowedBlocks::filter(array_map([$this, 'slug'], $blocks)); + + return array_intersect_key($blocks, $allowed); + } + + /** + * The namespaced block name from a `block.json`, e.g. `yard/greeting`. * * Returns null for a folder that holds no readable metadata with a namespaced * block name, so a stray directory is skipped rather than fatal. */ - private function slug(string $metadataPath): ?string + private function blockName(string $metadataPath): ?string { if (! file_exists($metadataPath)) { return null; @@ -90,42 +107,62 @@ private function slug(string $metadataPath): ?string $metadata = json_decode((string) file_get_contents($metadataPath), true); $name = is_array($metadata) && is_string($metadata['name'] ?? null) ? $metadata['name'] : ''; - $separator = strpos($name, '/'); - if (false === $separator) { - return null; - } + return '' === $this->slug($name) ? null : $name; + } - $slug = substr($name, $separator + 1); + /** + * The slug of a block name, e.g. `greeting` for `yard/greeting`. + */ + private function slug(string $blockName): string + { + $separator = strpos($blockName, '/'); - return '' === $slug ? null : $slug; + return false === $separator ? '' : substr($blockName, $separator + 1); + } + + /** + * The class WordPress generates for a block, e.g. `wp-block-yard-greeting`. + * + * Going through core's function rather than building the string ourselves + * means a site filtering `block_default_classname` gets `$blockClass` and + * every BEM modifier built on it following along. The function is marked + * `@access private`, hence the guard. + * + * @see wp_get_block_default_classname() + */ + private function blockClass(string $blockName): string + { + return function_exists('wp_get_block_default_classname') + ? (string) \wp_get_block_default_classname($blockName) + : 'wp-block-' . str_replace('/', '-', $blockName); } /** * Build the render callback for a single block. * + * The view model assembles the render data, including the wrapper attributes: * `get_block_wrapper_attributes()` reads the block currently being rendered, - * so it has to be called inside the callback rather than up front. + * so it has to run inside the callback rather than up front. Blocks without + * their own view model get a plain `BlockViewModel`, which adds nothing. */ - private function renderCallback(string $directory, string $slug, string $blockPath): callable + private function renderCallback(string $directory, string $blockName, string $blockPath): callable { - $templatePath = $blockPath . '/' . $slug . '.blade.php'; + $templatePath = $blockPath . '/' . $this->slug($blockName) . '.blade.php'; $viewModelClass = __NAMESPACE__ . '\\' . $directory . '\\' . $directory; - - return function ($attributes, $content = '', $block = null) use ($templatePath, $viewModelClass) { - $data = [ - 'attributes' => is_array($attributes) ? $attributes : [], - 'content' => $content, - 'block' => $block, - 'wrapperAttributes' => \get_block_wrapper_attributes(), - ]; - - // Blocks without a view model render on this data alone. - if (is_subclass_of($viewModelClass, BlockViewModel::class)) { - $data = (new $viewModelClass())->compose($data); - } - - return $this->renderer->render($templatePath, $data); + $blockClass = $this->blockClass($blockName); + + return function ($attributes, $content = '', $block = null) use ($templatePath, $viewModelClass, $blockClass) { + $viewModel = is_subclass_of($viewModelClass, BlockViewModel::class) + ? new $viewModelClass() + : new BlockViewModel(); + + return $this->renderer->render($templatePath, $viewModel->compose( + is_array($attributes) ? $attributes : [], + is_string($content) ? $content : '', + $block instanceof \WP_Block ? $block : null, + $blockClass + )); }; } } From fa088a78fbd0b654d5649cc1ee9f6ee2d56d010d Mon Sep 17 00:00:00 2001 From: Yannic van Veen Date: Thu, 3 Sep 2026 14:08:42 +0200 Subject: [PATCH 4/6] feat: add default css --- package-lock.json | 4 ++-- src/PhpBlocks/Greeting/block.json | 1 + src/PhpBlocks/Greeting/style.css | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/PhpBlocks/Greeting/style.css diff --git a/package-lock.json b/package-lock.json index 8119eba..a38614c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "yard-gutenberg", - "version": "1.6.2", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yard-gutenberg", - "version": "1.6.2", + "version": "1.8.0", "license": "MIT License", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/src/PhpBlocks/Greeting/block.json b/src/PhpBlocks/Greeting/block.json index 6da3928..8e84df9 100644 --- a/src/PhpBlocks/Greeting/block.json +++ b/src/PhpBlocks/Greeting/block.json @@ -8,6 +8,7 @@ "icon": "smiley", "description": "Toont een begroeting.", "textdomain": "yard", + "style": "file:./style.css", "supports": { "autoRegister": true, "html": false diff --git a/src/PhpBlocks/Greeting/style.css b/src/PhpBlocks/Greeting/style.css new file mode 100644 index 0000000..2493c0f --- /dev/null +++ b/src/PhpBlocks/Greeting/style.css @@ -0,0 +1,19 @@ +@layer plugins { + .wp-block-yard-greeting { + color: var( --yard-greeting-color, inherit ); + font-size: var( --yard-greeting-font-size, inherit ); + line-height: var( --yard-greeting-line-height, inherit ); + + &:has(.wp-block-yard-greeting__suffix) { + display: flex; + flex-direction: column; + row-gap: var( --yard-greeting-spacing, 0 ); + } + } + + .wp-block-yard-greeting__greeting { + color: var( --yard-greeting-greeting-color, inherit ); + font-size: var( --yard-greeting-greeting-font-size, 2rem ); + font-weight: var( --yard-greeting-greeting-font-weight, 700 ); + } +} From e76e6cbe18c289798c01553952ebf89b60924ffb Mon Sep 17 00:00:00 2001 From: Yannic van Veen Date: Thu, 3 Sep 2026 14:32:20 +0200 Subject: [PATCH 5/6] feat: add alignment option --- src/PhpBlocks/Greeting/block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpBlocks/Greeting/block.json b/src/PhpBlocks/Greeting/block.json index 8e84df9..0758075 100644 --- a/src/PhpBlocks/Greeting/block.json +++ b/src/PhpBlocks/Greeting/block.json @@ -8,8 +8,8 @@ "icon": "smiley", "description": "Toont een begroeting.", "textdomain": "yard", - "style": "file:./style.css", "supports": { + "align": true, "autoRegister": true, "html": false }, From f4314400feb55bbc23dd7de80782f3ad75e5987a Mon Sep 17 00:00:00 2001 From: Yannic van Veen Date: Thu, 3 Sep 2026 14:33:17 +0200 Subject: [PATCH 6/6] feat: return style to block.json --- src/PhpBlocks/Greeting/block.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PhpBlocks/Greeting/block.json b/src/PhpBlocks/Greeting/block.json index 0758075..72f9e67 100644 --- a/src/PhpBlocks/Greeting/block.json +++ b/src/PhpBlocks/Greeting/block.json @@ -19,5 +19,6 @@ "type": "string", "default": "" } - } + }, + "style": "file:./style.css" }