diff --git a/.devcontainer/README.md b/.devcontainer/README.md index d92fb209..c6ae07de 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,17 +1,20 @@ # Theme Dev Container -A **theme-only** dev container so you can work on WDS BT without installing PHP or Node locally—whether you’re at WDS (using the project template) or anywhere else. The stack matches [WebDevStudios/wds-devcontainer](https://github.com/WebDevStudios/wds-devcontainer): **PHP 8.2** and **Node 24**. +This is the **supported development environment** for WDS BT: a theme-only container so you do not need PHP or Node installed on the host. The stack matches [WebDevStudios/wds-devcontainer](https://github.com/WebDevStudios/wds-devcontainer): **PHP 8.2** and **Node 24**. + +PHP linting uses Composer scripts that call **`vendor/bin/phpcs`** and **`vendor/bin/phpcbf`** (`composer run-script phpcs` / `phpcs-fix`)—no extra wrapper scripts. Run those inside the container (or any Linux/macOS shell with Composer deps installed) for consistent results. ## Usage 1. Open this theme repo in Cursor or VS Code. -2. Choose **Reopen in Container** (or run **Dev Containers: Reopen in Container** from the command palette). -3. After the container builds, run `npm run setup` if needed (postCreate runs `npm ci && composer install`). +2. Choose **Reopen in Container** (or **Dev Containers: Reopen in Container** from the command palette). +3. After the container builds, `postCreateCommand` runs `npm ci && composer install`. Use `npm run setup` from the theme root when you need a full clean install and build. ## What’s included - PHP 8.2 with Composer and extensions: tokenizer, xmlwriter, simplexml, dom, mbstring - Node 24 +- **jq** (installed on container create) so `composer install` and Cursor-rules sync from `composer.json` succeed - VS Code extensions: Intelephense, ESLint, Prettier, Stylelint -When you use a full WordPress project (e.g. WDS project template), that project’s devcontainer is used for the site; this one is for standalone theme development only. +When you use a full WordPress project (e.g. WDS project template), that project’s devcontainer runs the site; this one is for standalone theme development only. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 66264868..cd62e216 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,6 +24,7 @@ ] } }, + "onCreateCommand": "sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq jq", "postCreateCommand": "npm ci && composer install --no-interaction", "remoteUser": "vscode" } diff --git a/.env b/.env index 1c8b5d5f..a4ad3876 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ # The version of your theme, manually updated by you. # This version will be reflected in style.css, package.json, and composer.json. -VERSION=1.4.5 +VERSION=1.4.6 diff --git a/.npmrc b/.npmrc index 8c2b99a2..30b2586a 100644 --- a/.npmrc +++ b/.npmrc @@ -1,5 +1,7 @@ # Allow legacy peer deps for WordPress ecosystem compatibility legacy-peer-deps=true -# Ensure scripts run with proper shell -script-shell=/bin/bash +# Do not set script-shell to /bin/bash here: that path does not exist on Windows and +# causes `spawn /bin/bash ENOENT` during dependency install scripts. Use the platform +# default (cmd.exe on Windows, sh on Unix). Unix/macOS developers can set +# `script-shell` in ~/.npmrc if they need bash for all npm scripts. diff --git a/README.md b/README.md index baed0f3e..88e38264 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # WDS BT -## Version: 1.4.5 +## Version: 1.4.6 [![WebDevStudios. Your Success is Our Mission.](https://webdevstudios.com/wp-content/uploads/2024/02/wds-banner.png)](https://webdevstudios.com/contact/) -### What's New in 1.4.0 +### What's New in 1.4.6 - **Block Showcase**: Powerful development tool (admin-only) to discover, preview, and inspect all registered blocks with their attributes in an organized, interactive format. - **Template-specific style loading**: Template and CPT CSS are built separately and enqueued only when the current request matches (404, search, archive, page templates, custom post types). Styles in `build/css/templates/` are discovered automatically—no PHP config needed. Reduces unused CSS for better Lighthouse scores. @@ -53,7 +53,7 @@ WDS BT is a foundational WordPress block theme designed for maximum flexibility | Responsive Design | Ensures optimal display and functionality across devices. | | Foundation Theme | Flexible base theme optimized for extensive customization. | | Automated Code Quality | Modern linting configurations with PHP 8.3 compatibility, ESLint 9 flat config, WordPress coding standards, and automated quality checks. | -| DevContainer | [.devcontainer](.devcontainer) for **theme-only** workflow: PHP 8.2, Node 24, Composer. Usable by anyone (not just WDS); matches [wds-devcontainer](https://github.com/WebDevStudios/wds-devcontainer) when using the project template. | +| DevContainer | **Recommended:** [.devcontainer](.devcontainer) for theme development (PHP 8.2, Node 24, Composer). This is the standard environment for active development on this repo. Matches [wds-devcontainer](https://github.com/WebDevStudios/wds-devcontainer) when using the project template. | | Third-party Block Style Overrides | Conditionally enqueue and override third-party block styles for efficient asset delivery. | | Accessibility Compliance | Built-in WCAG 2.2 compliance with automated Pa11y checks. | | Enhanced Webpack Configuration | Refined Webpack setup for improved dependency resolution and optimized asset management. | @@ -70,19 +70,19 @@ WDS BT is a foundational WordPress block theme designed for maximum flexibility ## Requirements - WordPress 6.4+ (tested upto 6.9.1) -- PHP 8.2+ (fully tested with PHP 8.4) -- [NPM](https://npmjs.com) (v11+) -- [Node](https://nodejs.org) (v25+) -- [Composer 2+](https://getcomposer.org/) +- **Development:** use the **[dev container](.devcontainer)** (supported path for PHP, Node, Composer, and linting). It ships PHP 8.2 and Node 24 aligned with this theme. +- PHP 8.2+ and [Composer 2+](https://getcomposer.org/) when running outside the container (CI, deployment, or contributor preference) +- [NPM](https://npmjs.com) (v11+) and [Node](https://nodejs.org) (v24+ per `package.json` engines) for build scripts - License: [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) ## Getting Started -1. Clone this repository to your WordPress theme directory (`wp-content/themes/`). -2. Activate WDS BT from your WordPress admin panel under Appearance > Themes. -3. Run `npm run setup` to install dependencies and perform an initial build. +1. Clone this repository into your WordPress theme directory (`wp-content/themes/`), or symlink it there. +2. **Open in the dev container:** In Cursor or VS Code, choose **Reopen in Container** so PHP, Node, and Composer match the project (see [.devcontainer/README.md](.devcontainer/README.md)). +3. From the theme root, run `npm run build` (after `postCreateCommand`’s `npm ci && composer install`). Use `npm run setup` when you need a full reset and install from scratch. +4. Activate WDS BT from your WordPress admin under **Appearance → Themes**. -**Theme-only dev (no local PHP/Node):** Open this repo in Cursor or VS Code and choose **Reopen in Container**. The [.devcontainer](.devcontainer) provides PHP 8.2 and Node 24 so you can run `npm run setup` and build without installing tooling locally—works for everyone, not only WDS projects. +Contributors should use the dev container for day-to-day work so PHPCS, ESLint, and builds share one environment. PHP coding standards run through Composer (`composer run-script phpcs`) and invoke **`vendor/bin/phpcs`** directly. ## Documentation @@ -129,7 +129,7 @@ Register block styles in `inc/hooks/register-block-styles.php`. Override core bl ## Linting & Lefthook -PHP (PHPCS), JavaScript (ESLint), and CSS/SCSS (Stylelint) use the latest WordPress standards; warnings are treated as failures. On commit, Lefthook runs auto-fix, re-stages changed files, then runs strict lint. Do not use `--no-verify`; CI runs the same checks. Full config and policy: **[docs/linting-and-hooks.md](docs/linting-and-hooks.md)**. +PHP (PHPCS), JavaScript (ESLint), and CSS/SCSS (Stylelint) use the latest WordPress standards; warnings are treated as failures. Composer scripts invoke **`vendor/bin/phpcs`** and **`vendor/bin/phpcbf`** (see `composer.json`). On commit, Lefthook runs auto-fix, re-stages changed files, then runs strict lint. Do not use `--no-verify`; CI runs the same checks. Full config and policy: **[docs/linting-and-hooks.md](docs/linting-and-hooks.md)**. ## Dynamic Block Pattern Categories @@ -137,7 +137,7 @@ Block pattern categories are registered from subfolders of `patterns/`. Add a fo ## Accessibility & Quality -Run `npm run a11y` for Pa11y-based accessibility checks and `npm run lighthouse` for performance and SEO scores. CI runs security and code-quality checks. Cross-platform notes (rimraf, PHP detection) are in **[docs/accessibility-and-quality.md](docs/accessibility-and-quality.md)**. +Run `npm run a11y` for Pa11y-based accessibility checks and `npm run lighthouse` for performance and SEO scores. CI runs security and code-quality checks. Additional tooling notes are in **[docs/accessibility-and-quality.md](docs/accessibility-and-quality.md)**. ## Contributing and Support diff --git a/assets/scss/base/forms.scss b/assets/scss/base/forms.scss index 220ca548..1eb76a32 100644 --- a/assets/scss/base/forms.scss +++ b/assets/scss/base/forms.scss @@ -13,6 +13,8 @@ textarea { } select { + -webkit-appearance: none; + -moz-appearance: none; appearance: none; outline: 0; background: url(../images/icons/icon-arrow-down.svg) no-repeat right 0.8em diff --git a/assets/scss/templates/block-showcase.scss b/assets/scss/templates/block-showcase.scss index 9e7ef419..0b4bd403 100644 --- a/assets/scss/templates/block-showcase.scss +++ b/assets/scss/templates/block-showcase.scss @@ -6,15 +6,16 @@ @use '../abstracts/responsive-mixins'; -// Block showcase styles. +// Block showcase styles (palette: base, contrast, accent-1 … accent-3 from theme.json). .wdsbt-block-showcase { + color: var(--wp--preset--color--contrast); + .wdsbt-showcase-section-heading { - color: var(--wp--preset--color--contrast, #000); + color: var(--wp--preset--color--contrast); margin-top: var(--wp--preset--spacing--60, 3rem); margin-bottom: var(--wp--preset--spacing--40, 2rem); padding-bottom: var(--wp--preset--spacing--20, 1rem); - border-bottom: 2px solid - var(--wp--preset--color--contrast-2, rgba(0, 0, 0, 0.1)); + border-bottom: 2px solid var(--wp--preset--color--accent-1); &:first-child { margin-top: 0; @@ -41,7 +42,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: var(--wp--preset--spacing--40); + padding: var(--wp--preset--spacing--20); background: transparent; border: none; cursor: pointer; @@ -49,10 +50,17 @@ font-size: inherit; font-weight: 600; color: inherit; - transition: background-color 0.2s ease; + transition: + background-color 0.2s ease, + color 0.2s ease; &:hover { - background: var(--wp--preset--color--base-2, #f5f5f5); + background: var(--wp--preset--color--accent-2); + } + + &:focus-visible { + outline: 2px solid var(--wp--preset--color--accent-1); + outline-offset: 2px; } .wp-block-accordion-heading__toggle-title { @@ -62,6 +70,7 @@ .wp-block-accordion-heading__toggle-icon { font-size: 1.5rem; line-height: 1; + color: var(--wp--preset--color--accent-1); transition: transform 0.2s ease; margin-left: var(--wp--preset--spacing--20); } @@ -69,6 +78,7 @@ &[aria-expanded='true'] { .wp-block-accordion-heading__toggle-icon { transform: rotate(45deg); + color: var(--wp--preset--color--contrast); } } } @@ -103,9 +113,9 @@ display: flex; flex-direction: column; overflow: hidden; - border: 1px solid rgba(0, 0, 0, 0.12); + border: 1px solid var(--wp--preset--color--accent-2); border-radius: var(--wp--preset--border-radius--sm, 6px); - background: var(--wp--preset--color--base, #fff); + background: var(--wp--preset--color--base); min-width: 0; } @@ -116,8 +126,8 @@ font-weight: 600; line-height: 1.2; letter-spacing: 0.02em; - color: #fff; - background: #000; + color: var(--wp--preset--color--contrast); + background: var(--wp--preset--color--accent-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -153,51 +163,63 @@ } .wdsbt-showcase-block-card { - border: 1px solid - var(--wp--preset--color--contrast, rgba(0, 0, 0, 0.1)); + border: 1px solid var(--wp--preset--color--accent-2); border-radius: var(--wp--preset--border-radius--md); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + background: var(--wp--preset--color--base); + box-shadow: 0 1px 2px + color-mix( + in srgb, + var(--wp--preset--color--contrast) 10%, + transparent + ); padding: var(--wp--preset--spacing--50); overflow: hidden; display: flex; flex-direction: column; + scroll-margin-top: var(--wp--preset--spacing--70, 4rem); .wdsbt-showcase-block-title { margin-top: 0; - margin-bottom: var(--wp--preset--spacing--30); + margin-bottom: 0; } .wdsbt-showcase-block-meta { - margin-bottom: var(--wp--preset--spacing--40); - padding-bottom: var(--wp--preset--spacing--40); - border-bottom: 1px solid - var(--wp--preset--color--contrast, rgba(0, 0, 0, 0.1)); + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: var(--wp--preset--spacing--30); + margin-bottom: var(--wp--preset--spacing--30); + padding-bottom: var(--wp--preset--spacing--10); + border-bottom: 1px solid var(--wp--preset--color--accent-2); .wdsbt-showcase-block-name { - color: var( - --wp--preset--color--contrast, - rgba(0, 0, 0, 0.6) - ); - font-size: var(--wp--preset--font-size--small); - padding: var(--wp--preset--spacing--30); - border-radius: var(--wp--preset--border-radius--sm); + font-family: var(--wp--preset--font-family--mono); + font-size: var(--wp--preset--font-size--x-small); + font-weight: 400; + color: var(--wp--preset--color--contrast); + + &::before { + content: '📦'; + display: inline-block; + margin-right: 0.5rem; + transition: transform 0.2s ease; + font-size: 0.75rem; + color: var(--wp--preset--color--accent-1); + } } } .wdsbt-showcase-block-attributes { - margin-bottom: var(--wp--preset--spacing--40); - padding-bottom: var(--wp--preset--spacing--40); - border-bottom: 1px solid - var(--wp--preset--color--contrast, rgba(0, 0, 0, 0.1)); + margin-bottom: var(--wp--preset--spacing--30); + padding-bottom: var(--wp--preset--spacing--30); + border-bottom: 1px solid var(--wp--preset--color--accent-2); .wdsbt-attributes-details { .wdsbt-attributes-summary { cursor: pointer; font-size: var(--wp--preset--font-size--small); - color: var( - --wp--preset--color--contrast, - rgba(0, 0, 0, 0.7) - ); + color: var(--wp--preset--color--contrast); user-select: none; list-style: none; @@ -211,6 +233,7 @@ margin-right: 0.5rem; transition: transform 0.2s ease; font-size: 0.75rem; + color: var(--wp--preset--color--accent-1); } } @@ -237,22 +260,16 @@ .wdsbt-attribute-name { display: inline-block; font-weight: 600; - color: var(--wp--preset--color--foreground, #000); + color: var(--wp--preset--color--contrast); margin-right: var(--wp--preset--spacing--10); } .wdsbt-attribute-type { display: inline-block; padding: 0.125rem 0.375rem; - background: var( - --wp--preset--color--base-2, - #f5f5f5 - ); + background: var(--wp--preset--color--accent-2); border-radius: 3px; - color: var( - --wp--preset--color--contrast, - rgba(0, 0, 0, 0.6) - ); + color: var(--wp--preset--color--contrast); margin-right: var(--wp--preset--spacing--10); font-size: 0.7rem; } @@ -262,10 +279,7 @@ display: block; margin-top: 0.25rem; margin-left: var(--wp--preset--spacing--20); - color: var( - --wp--preset--color--contrast, - rgba(0, 0, 0, 0.6) - ); + color: var(--wp--preset--color--accent-3); font-style: italic; } } @@ -277,6 +291,94 @@ height: fit-content; max-width: 100%; } + + .wdsbt-showcase-variations { + margin-top: var(--wp--preset--spacing--30); + padding-top: var(--wp--preset--spacing--20); + border-top: 1px solid var(--wp--preset--color--accent-2); + + &__summary { + cursor: pointer; + font-size: var(--wp--preset--font-size--small); + font-weight: 600; + color: var(--wp--preset--color--contrast); + user-select: none; + list-style: none; + + &::-webkit-details-marker { + display: none; + } + + &::before { + content: '▶'; + display: inline-block; + margin-right: 0.5rem; + transition: transform 0.2s ease; + font-size: 0.75rem; + color: var(--wp--preset--color--accent-1); + } + } + + &[open] .wdsbt-showcase-variations__summary::before { + transform: rotate(90deg); + } + + &__grid { + display: grid; + grid-template-columns: repeat( + auto-fill, + minmax(14rem, 1fr) + ); + gap: var(--wp--preset--spacing--30); + margin-top: var(--wp--preset--spacing--20); + } + } + + .wdsbt-showcase-variation-card { + padding: var(--wp--preset--spacing--20); + border: 1px solid var(--wp--preset--color--accent-2); + border-radius: 4px; + background: color-mix( + in srgb, + var(--wp--preset--color--base) 92%, + transparent + ); + + &__meta { + margin-bottom: var(--wp--preset--spacing--20); + } + + &__kind { + display: inline-block; + margin-bottom: 0.35rem; + padding: 0.1rem 0.35rem; + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--wp--preset--color--contrast); + background: var(--wp--preset--color--accent-2); + border-radius: 2px; + } + + &__title { + margin: 0 0 0.25rem; + font-size: var(--wp--preset--font-size--small); + font-weight: 600; + color: var(--wp--preset--color--contrast); + } + + &__name { + font-size: 0.7rem; + word-break: break-all; + color: var(--wp--preset--color--accent-3); + } + + &__preview { + max-width: 100%; + overflow: auto; + } + } } } } diff --git a/composer.json b/composer.json index 96a6bd76..c727f2e0 100644 --- a/composer.json +++ b/composer.json @@ -33,14 +33,10 @@ "wp-coding-standards/wpcs": "^3.3" }, "scripts": { - "phpcs": [ - "./scripts/run-phpcs.sh --report=full,source" - ], - "phpcs-fix": [ - "./scripts/run-phpcbf.sh --report=summary,source" - ], + "phpcs": "vendor/bin/phpcs --runtime-set ignore_warnings_on_exit 0 --report=full,source", + "phpcs-fix": "vendor/bin/phpcbf --report=summary,source", "post-install-cmd": [ - "./scripts/update-cursorrules.sh" + "node scripts/theme-tools.cjs cursorrules" ] } } diff --git a/composer.lock b/composer.lock index b16754f7..d46a8f52 100644 --- a/composer.lock +++ b/composer.lock @@ -297,16 +297,16 @@ }, { "name": "composer/composer", - "version": "2.9.5", + "version": "2.9.7", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "72a8f8e653710e18d83e5dd531eb5a71fc3223e6" + "reference": "82a2fbd1372a98d7915cfb092acf05207d9b4113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/72a8f8e653710e18d83e5dd531eb5a71fc3223e6", - "reference": "72a8f8e653710e18d83e5dd531eb5a71fc3223e6", + "url": "https://api.github.com/repos/composer/composer/zipball/82a2fbd1372a98d7915cfb092acf05207d9b4113", + "reference": "82a2fbd1372a98d7915cfb092acf05207d9b4113", "shasum": "" }, "require": { @@ -394,7 +394,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.9.5" + "source": "https://github.com/composer/composer/tree/2.9.7" }, "funding": [ { @@ -406,7 +406,7 @@ "type": "github" } ], - "time": "2026-01-29T10:40:53+00:00" + "time": "2026-04-14T11:31:52+00:00" }, { "name": "composer/metadata-minifier", @@ -1235,16 +1235,16 @@ }, { "name": "mck89/peast", - "version": "v1.17.5", + "version": "v1.17.6", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "e19a8bd896b7f04941a38fd38a140c9a6531c84f" + "reference": "b8b4184b1e6912669f9af155caef9050509d9f18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/e19a8bd896b7f04941a38fd38a140c9a6531c84f", - "reference": "e19a8bd896b7f04941a38fd38a140c9a6531c84f", + "url": "https://api.github.com/repos/mck89/peast/zipball/b8b4184b1e6912669f9af155caef9050509d9f18", + "reference": "b8b4184b1e6912669f9af155caef9050509d9f18", "shasum": "" }, "require": { @@ -1257,7 +1257,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17.5-dev" + "dev-master": "1.17.6-dev" } }, "autoload": { @@ -1278,9 +1278,9 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.17.5" + "source": "https://github.com/mck89/peast/tree/v1.17.6" }, - "time": "2026-03-15T10:47:07+00:00" + "time": "2026-04-24T08:04:05+00:00" }, { "name": "nb/oxymel", @@ -2448,16 +2448,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -2507,7 +2507,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -2527,20 +2527,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -2589,7 +2589,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -2609,11 +2609,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -2674,7 +2674,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -2698,16 +2698,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -2759,7 +2759,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -2779,11 +2779,11 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -2839,7 +2839,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" }, "funding": [ { @@ -2863,16 +2863,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -2923,7 +2923,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -2943,11 +2943,11 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -3003,7 +3003,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" }, "funding": [ { @@ -3027,16 +3027,16 @@ }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { @@ -3083,7 +3083,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" }, "funding": [ { @@ -3103,7 +3103,7 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { "name": "symfony/service-contracts", @@ -5492,16 +5492,16 @@ }, { "name": "wp-cli/wp-config-transformer", - "version": "v1.4.5", + "version": "v1.4.6", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-config-transformer.git", - "reference": "8f5e66c717a7371dfb6559086880bee528aee858" + "reference": "1ef18784990b85b35202c2d68dbbc4a8fe6615b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-config-transformer/zipball/8f5e66c717a7371dfb6559086880bee528aee858", - "reference": "8f5e66c717a7371dfb6559086880bee528aee858", + "url": "https://api.github.com/repos/wp-cli/wp-config-transformer/zipball/1ef18784990b85b35202c2d68dbbc4a8fe6615b6", + "reference": "1ef18784990b85b35202c2d68dbbc4a8fe6615b6", "shasum": "" }, "require": { @@ -5535,9 +5535,9 @@ "homepage": "https://github.com/wp-cli/wp-config-transformer", "support": { "issues": "https://github.com/wp-cli/wp-config-transformer/issues", - "source": "https://github.com/wp-cli/wp-config-transformer/tree/v1.4.5" + "source": "https://github.com/wp-cli/wp-config-transformer/tree/v1.4.6" }, - "time": "2026-03-20T07:28:10+00:00" + "time": "2026-04-10T07:32:03+00:00" }, { "name": "wp-coding-standards/wpcs", diff --git a/docs/block-showcase.md b/docs/block-showcase.md index e0f1b976..255a433e 100644 --- a/docs/block-showcase.md +++ b/docs/block-showcase.md @@ -12,6 +12,10 @@ Admin-only development tool to discover, preview, and inspect all registered blo Blocks grouped by category; for each block: name, identifier, attributes (with types/defaults/options), and a live preview. Content comes from block `example`, defaults, or generated markup. Some blocks (e.g. `core/legacy-widget`, `core/freeform`) are skipped. +**Block styles** from `block.json` (`styles`, e.g. Separator “Wide Line” / “Dots”) are listed first with a **Style** label; each non-default style gets a preview by merging `is-style-{name}` into the block’s saved markup (`attrs.className` plus the root element in `innerHTML` / `innerContent` when needed) so static blocks like `core/social-links` actually render the correct layout classes. + +**Variations** from `WP_Block_Type::get_variations()` (PHP / `block.json` `variations`) appear with a **Variation** label. Variations registered only in JavaScript (`registerBlockVariation`) are not visible to PHP and do not appear here. + ## Security Template and shortcode are restricted to `manage_options`. Non-admins visiting a Block Showcase template get a 404. diff --git a/inc/functions/block-processor-utils.php b/inc/functions/block-processor-utils.php index 3bb411f0..412a487c 100644 --- a/inc/functions/block-processor-utils.php +++ b/inc/functions/block-processor-utils.php @@ -7,7 +7,7 @@ * * @see https://make.wordpress.org/core/2025/11/19/introducing-the-streaming-block-parser-in-wordpress-6-9/ * - * @package wdsbt + * @package WDSBT */ namespace WebDevStudios\wdsbt; diff --git a/inc/functions/block-showcase.php b/inc/functions/block-showcase.php index 81c51c2d..d1466c99 100644 --- a/inc/functions/block-showcase.php +++ b/inc/functions/block-showcase.php @@ -7,15 +7,18 @@ * * @see https://make.wordpress.org/core/2025/11/19/introducing-the-streaming-block-parser-in-wordpress-6-9/ * - * @package wdsbt + * @package WDSBT */ namespace WebDevStudios\wdsbt; /** - * Theme color palette from theme.json (not core defaults). + * Get the color palette from theme.json (theme-defined colors only, no core defaults). * - * @return array List of entries with slug, name, and color. + * Uses the theme's theme.json so only Base, Primary, Accent, etc. are shown— + * not WordPress core palette (cyan, blush, gray, etc.). + * + * @return array List of color entries with 'slug', 'name', 'color' keys. */ function get_theme_json_color_palette() { if ( ! class_exists( 'WP_Theme_JSON_Resolver' ) ) { @@ -31,6 +34,7 @@ function get_theme_json_color_palette() { } $raw = $settings['color']['palette']; + // Theme data may expose palette as flat or as [ 'theme' => [ ... ] ]. $list = array(); $first = reset( $raw ); if ( is_array( $first ) && isset( $first['color'] ) ) { @@ -135,12 +139,6 @@ function get_all_registered_blocks() { continue; } - // Omit child-only blocks (block.json parent), e.g. FAQ inside FAQs, list-item inside list. - $parent_blocks = isset( $block_type->parent ) ? $block_type->parent : null; - if ( is_array( $parent_blocks ) && array() !== $parent_blocks ) { - continue; - } - $parts = explode( '/', $block_name ); if ( count( $parts ) !== 2 ) { continue; @@ -242,7 +240,7 @@ function get_block_showcase_content( $block_name, $block_type ) { } $core_defaults = array( - 'core/paragraph' => '

This is a paragraph block with formatted text, a link, and emphasis.

', + 'core/paragraph' => '

This is a paragraph block with formatted text and emphasis.

', 'core/heading' => '

Heading H1

Heading H2

Heading H3

Heading H4

Heading H5
Heading H6
', 'core/list' => '', 'core/quote' => '

This is a quote block for highlighting important statements.

Citation
', @@ -261,7 +259,7 @@ function get_block_showcase_content( $block_name, $block_type ) { 'core/gallery' => '', 'core/audio' => '
', 'core/accordion' => '

Words these example words these example are example these example are words. These example these example words example words are words are these example these words these. Example words example are example these example are example. Example words these are these words example these words are these words. Words these are example these example these are.

Are these example are example these are words example are words are. Example are these example words example are these are example. Example are words these are words are words these are. These are example these example these words are words are. Are example these example words example are example words are these words these.

  • Are words these words example words are these are these words.
  • Are these example are these words these words example these example are these are these.
  • Example are these example words these example are these words.
  • Are words example are words example these example these example words example.
', - 'core/cover' => '

Cover Block

', + 'core/cover' => '

Cover Block

', 'core/file' => '
Download sample.pdf
', 'core/media-text' => '
Media & Text

Media & Text Block. Example these example are words are words are example are example these. Are these example these words example are these words these example. Words are example words these are example words. These are words are words example are example words are words are words these. Are words example words are example words these example these are example.

', 'core/video' => '
Video Caption
', @@ -314,25 +312,418 @@ function get_block_showcase_content( $block_name, $block_type ) { } /** - * Render a block for the showcase using WP_Block_Processor. + * Merges variation attributes into the first matching block in a parsed tree. * - * @param string $block_name The fully qualified block name. - * @param object $block_type The block type object. - * @return string Rendered block HTML. + * @param array[] $blocks Parsed blocks (by reference). + * @param string $block_name Block name to match (e.g. core/button). + * @param array $attributes Attributes to merge into attrs. + * @return bool True if a block was updated. + */ +function apply_variation_attributes_to_block_tree( array &$blocks, $block_name, array $attributes ) { + foreach ( $blocks as &$block ) { + if ( ! empty( $block['blockName'] ) && $block['blockName'] === $block_name ) { + $block['attrs'] = array_merge( (array) ( $block['attrs'] ?? array() ), $attributes ); + return true; + } + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { + if ( apply_variation_attributes_to_block_tree( $block['innerBlocks'], $block_name, $attributes ) ) { + return true; + } + } + } + unset( $block ); + return false; +} + +/** + * Default root CSS class for a block (matches saved markup, e.g. wp-block-social-links). + * + * @param string $block_name Block name (core/social-links, wdsbt/foo). + * @return string Class substring to locate in inner HTML, or empty. + */ +function get_showcase_block_root_css_class( $block_name ) { + $block_name = (string) $block_name; + if ( '' === $block_name || false === strpos( $block_name, '/' ) ) { + return ''; + } + if ( 0 === strpos( $block_name, 'core/' ) ) { + return str_replace( 'core/', 'wp-block-', $block_name ); + } + + return 'wp-block-' . str_replace( '/', '-', $block_name ); +} + +/** + * Injects a single is-style-* class on the first tag whose class list contains the block root class. + * Removes any existing is-style-* classes on that tag so one block style is active. + * + * @param string $markup HTML fragment. + * @param string $root_css_class Substring such as wp-block-social-links. + * @param string $style_class Full class, e.g. is-style-pill-shape (already sanitized). + * @return string Updated markup. + */ +function showcase_inject_block_style_into_markup( $markup, $root_css_class, $style_class ) { + if ( ! is_string( $markup ) || '' === $markup || '' === $root_css_class || '' === $style_class ) { + return $markup; + } + if ( false === strpos( $markup, $root_css_class ) ) { + return $markup; + } + + $processor = new \WP_HTML_Tag_Processor( $markup ); + while ( $processor->next_tag() ) { + $class = $processor->get_attribute( 'class' ); + if ( ! is_string( $class ) || false === strpos( $class, $root_css_class ) ) { + continue; + } + $classes = array_filter( preg_split( '/\s+/', $class, -1, PREG_SPLIT_NO_EMPTY ) ); + $classes = array_values( + array_filter( + $classes, + static function ( $token ) { + return ! preg_match( '/^is-style-/', $token ); + } + ) + ); + if ( ! in_array( $style_class, $classes, true ) ) { + $classes[] = $style_class; + } + $processor->set_attribute( 'class', implode( ' ', $classes ) ); + break; + } + + return $processor->get_updated_html(); +} + +/** + * Merges className attribute: strip is-style-*, add one block style class. + * + * @param array $attrs Block attrs. + * @param string $style_class Full is-style-* class. + * @return array Updated attrs. + */ +function showcase_merge_block_style_classname_attr( array $attrs, $style_class ) { + $existing = isset( $attrs['className'] ) ? (string) $attrs['className'] : ''; + $classes = array_filter( preg_split( '/\s+/', $existing, -1, PREG_SPLIT_NO_EMPTY ) ); + $classes = array_values( + array_filter( + $classes, + static function ( $token ) { + return ! preg_match( '/^is-style-/', $token ); + } + ) + ); + if ( ! in_array( $style_class, $classes, true ) ) { + $classes[] = $style_class; + } + $attrs['className'] = implode( ' ', $classes ); + return $attrs; +} + +/** + * Applies a block style preview to one parsed block (attrs + innerHTML / innerContent). + * + * @param array $block Parsed block (by reference). + * @param string $block_name Expected blockName. + * @param string $style_class Full sanitized class, e.g. is-style-wide. + * @return bool True if this block was updated. + */ +function showcase_apply_block_style_to_parsed_block( array &$block, $block_name, $style_class ) { + if ( empty( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + return false; + } + + $root = get_showcase_block_root_css_class( $block_name ); + if ( '' === $root ) { + return false; + } + + $block['attrs'] = showcase_merge_block_style_classname_attr( (array) ( $block['attrs'] ?? array() ), $style_class ); + + if ( ! empty( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) && false !== strpos( $block['innerHTML'], $root ) ) { + $block['innerHTML'] = showcase_inject_block_style_into_markup( $block['innerHTML'], $root, $style_class ); + } + + if ( ! empty( $block['innerContent'] ) && is_array( $block['innerContent'] ) ) { + foreach ( $block['innerContent'] as $i => $chunk ) { + if ( is_string( $chunk ) && false !== strpos( $chunk, $root ) ) { + $block['innerContent'][ $i ] = showcase_inject_block_style_into_markup( $chunk, $root, $style_class ); + } + } + } + + $has_inner_placeholder = false; + if ( ! empty( $block['innerContent'] ) && is_array( $block['innerContent'] ) ) { + foreach ( $block['innerContent'] as $c ) { + if ( null === $c ) { + $has_inner_placeholder = true; + break; + } + } + } + if ( ! $has_inner_placeholder && isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ) { + $block['innerContent'] = array( $block['innerHTML'] ); + } + + return true; +} + +/** + * Applies a block style to the first matching block in a parsed tree (depth-first). + * + * @param array[] $blocks Parsed blocks (by reference). + * @param string $block_name Block name to match. + * @param string $style_class Full is-style-* class (sanitized). + * @return bool True if a block was updated. + */ +function showcase_apply_block_style_in_tree( array &$blocks, $block_name, $style_class ) { + foreach ( $blocks as &$block ) { + if ( showcase_apply_block_style_to_parsed_block( $block, $block_name, $style_class ) ) { + return true; + } + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { + if ( showcase_apply_block_style_in_tree( $block['innerBlocks'], $block_name, $style_class ) ) { + return true; + } + } + } + unset( $block ); + return false; +} + +/** + * Serialized showcase markup with a block style class applied (block.json `styles`, not variations). + * + * @param string $block_name Fully qualified block name. + * @param object $block_type Block type instance. + * @param string $style_slug Style `name` from block.json (e.g. wide, dots). + * @return string Serialized markup or empty string. */ -function render_block_for_showcase( $block_name, $block_type ) { - if ( is_string( $block_name ) && str_starts_with( $block_name, 'wpml/' ) ) { +function get_showcase_serialized_for_block_style( $block_name, $block_type, $style_slug ) { + $style_slug = is_string( $style_slug ) ? trim( $style_slug ) : ''; + if ( '' === $style_slug ) { + return ''; + } + + $base = get_block_showcase_content( $block_name, $block_type ); + if ( '' === $base ) { + return ''; + } + + $slug_sanitized = sanitize_html_class( str_replace( '/', '-', $style_slug ) ); + if ( '' === $slug_sanitized ) { + return ''; + } + $class_segment = 'is-style-' . $slug_sanitized; + + $blocks = parse_blocks( $base ); + if ( empty( $blocks ) || ! showcase_apply_block_style_in_tree( $blocks, $block_name, $class_segment ) ) { return ''; } - $block_content = get_block_showcase_content( $block_name, $block_type ); + return serialize_blocks( $blocks ); +} + +/** + * Rendered previews for each non-default block style (block.json `styles`). + * + * @param string $block_name Fully qualified block name. + * @param object $block_type Block type instance. + * @return array + */ +function get_block_showcase_style_previews( $block_name, $block_type ) { + if ( ! isset( $block_type->styles ) || ! is_array( $block_type->styles ) || empty( $block_type->styles ) ) { + return array(); + } + + $base = get_block_showcase_content( $block_name, $block_type ); + $out = array(); + + foreach ( $block_type->styles as $style ) { + if ( ! is_array( $style ) || empty( $style['name'] ) || ! is_string( $style['name'] ) ) { + continue; + } + if ( ! empty( $style['isDefault'] ) ) { + continue; + } + + $slug = $style['name']; + $serialized = get_showcase_serialized_for_block_style( $block_name, $block_type, $slug ); + if ( '' === $serialized || $serialized === $base ) { + continue; + } + + $html = render_block_for_showcase( $block_name, $block_type, $serialized ); + if ( '' === trim( $html ) ) { + continue; + } + + $title = isset( $style['label'] ) && is_string( $style['label'] ) ? $style['label'] : $slug; + + $out[] = array( + 'kind' => 'style', + 'name' => $slug, + 'title' => $title, + 'html' => $html, + ); + } + + return $out; +} + +/** + * Merges style previews (first) and variation previews for the showcase UI. + * + * @param string $block_name Fully qualified block name. + * @param object $block_type Block type instance. + * @return array + */ +function get_block_showcase_style_and_variation_previews( $block_name, $block_type ) { + return array_merge( + get_block_showcase_style_previews( $block_name, $block_type ), + get_block_showcase_variation_previews( $block_name, $block_type ) + ); +} + +/** + * Builds serialized block markup for a registered variation (PHP / block.json only). + * + * @param string $block_name Fully qualified block name. + * @param object $block_type WP_Block_Type instance. + * @param array $variation Variation definition (name, title, attributes, innerBlocks, …). + * @return string Serialized markup or empty string if nothing applicable. + */ +function get_showcase_serialized_for_variation( $block_name, $block_type, array $variation ) { + $base = get_block_showcase_content( $block_name, $block_type ); + if ( '' === $base ) { + return ''; + } + + $has_inner = ! empty( $variation['innerBlocks'] ) && is_array( $variation['innerBlocks'] ); + $has_attrs = isset( $variation['attributes'] ) && is_array( $variation['attributes'] ); + + if ( $has_inner ) { + $example = array( 'innerBlocks' => $variation['innerBlocks'] ); + if ( $has_attrs ) { + $example['attributes'] = $variation['attributes']; + } + $built = get_block_from_example( $block_name, $example ); + if ( '' !== $built ) { + return $built; + } + } + + if ( $has_attrs && ! empty( $variation['attributes'] ) ) { + $blocks = parse_blocks( $base ); + if ( ! empty( $blocks ) && apply_variation_attributes_to_block_tree( $blocks, $block_name, $variation['attributes'] ) ) { + return serialize_blocks( $blocks ); + } + $built = get_block_from_example( $block_name, array( 'attributes' => $variation['attributes'] ) ); + if ( '' !== $built ) { + return $built; + } + } + + return ''; +} + +/** + * Human-readable title for a block variation in the showcase. + * + * @param array $variation Variation definition. + * @return string + */ +function get_block_showcase_variation_title( array $variation ) { + if ( ! empty( $variation['title'] ) && is_string( $variation['title'] ) ) { + return $variation['title']; + } + if ( ! empty( $variation['name'] ) && is_string( $variation['name'] ) ) { + return $variation['name']; + } + return ''; +} + +/** + * Returns rendered previews for each server-registered variation that differs from the default showcase. + * + * Variations registered only in JavaScript are not available to PHP and are omitted. + * + * @param string $block_name Fully qualified block name. + * @param object $block_type Block type object. + * @return array + */ +function get_block_showcase_variation_previews( $block_name, $block_type ) { + if ( ! $block_type instanceof \WP_Block_Type || ! method_exists( $block_type, 'get_variations' ) ) { + return array(); + } + + $variations = $block_type->get_variations(); + if ( empty( $variations ) || ! is_array( $variations ) ) { + return array(); + } + + $base = get_block_showcase_content( $block_name, $block_type ); + $out = array(); + + foreach ( $variations as $variation ) { + if ( ! is_array( $variation ) || empty( $variation['name'] ) || ! is_string( $variation['name'] ) ) { + continue; + } + + $serialized = get_showcase_serialized_for_variation( $block_name, $block_type, $variation ); + if ( '' === $serialized || $serialized === $base ) { + continue; + } + + $html = render_block_for_showcase( $block_name, $block_type, $serialized ); + if ( '' === trim( $html ) ) { + continue; + } + + $title = get_block_showcase_variation_title( $variation ); + if ( '' === $title ) { + $title = $variation['name']; + } + + $out[] = array( + 'kind' => 'variation', + 'name' => $variation['name'], + 'title' => $title, + 'html' => $html, + ); + } + + return $out; +} + +/** + * Render a block for the showcase using WP_Block_Processor. + * + * @param string $block_name The fully qualified block name. + * @param object $block_type The block type object. + * @param string|null $block_content Optional serialized block markup; default uses showcase sample content. + * @return string Rendered block HTML. + */ +function render_block_for_showcase( $block_name, $block_type, $block_content = null ) { + if ( null === $block_content ) { + $block_content = get_block_showcase_content( $block_name, $block_type ); + } if ( empty( $block_content ) ) { return ''; } - $skip_blocks = array( 'core/legacy-widget', 'core/freeform' ); + $skip_blocks = array( + 'core/legacy-widget', + 'core/freeform', + // WPML parses saved block HTML for current vs other language nodes; showcase uses minimal markup only. + 'wpml/language-switcher', + 'wpml/navigation-language-switcher', + ); if ( in_array( $block_name, $skip_blocks, true ) ) { + if ( 'wpml/language-switcher' === $block_name || 'wpml/navigation-language-switcher' === $block_name ) { + return '

' . esc_html__( 'WPML language switcher blocks need full saved markup from the editor and cannot be previewed here.', 'wdsbt' ) . '

'; + } return '

This block type cannot be previewed in the showcase.

'; } @@ -421,6 +812,7 @@ function render_block_for_showcase( $block_name, $block_type ) { return $rendered; } else { + // Fallback if oEmbed fetch fails - show URL. return '

Embed preview not available. URL: ' . esc_html( $url ) . '

'; } } @@ -437,11 +829,7 @@ function render_block_for_showcase( $block_name, $block_type ) { } } - try { - $rendered = do_blocks( $block_content ); - } catch ( \Throwable $e ) { - return '

' . esc_html__( 'Preview could not be rendered.', 'wdsbt' ) . '

'; - } + $rendered = do_blocks( $block_content ); return $rendered; } @@ -459,6 +847,22 @@ function get_block_display_name( $block_name ) { return ucwords( $name ); } +/** + * HTML id for deep-linking to a block card on the showcase page (URL fragment). + * + * @param string $block_name The fully qualified block name (e.g. core/paragraph). + * @return string Safe unique id, e.g. block-showcase-core-paragraph. + */ +function get_block_showcase_anchor_id( $block_name ) { + $slug = str_replace( '/', '-', (string) $block_name ); + $slug = preg_replace( '/[^a-z0-9\-]+/i', '-', $slug ); + $slug = trim( preg_replace( '/-+/', '-', strtolower( $slug ) ), '-' ); + if ( '' === $slug ) { + $slug = 'block-' . substr( md5( $block_name ), 0, 8 ); + } + return 'block-showcase-' . $slug; +} + /** * Get formatted block attributes for display. * @@ -492,10 +896,10 @@ function get_block_attributes_info( $block_type ) { } /** - * Category slug for showcase grouping (block category, wdsbt, or other). + * Get block category for organization using WordPress's native categorization. * - * @param string $block_name Qualified name. - * @param object|null $block_type Optional; from registry when null. + * @param string $block_name The fully qualified block name. + * @param object $block_type The block type object. * @return string Category slug. */ function get_block_category( $block_name, $block_type = null ) { diff --git a/inc/hooks/block-showcase-shortcode.php b/inc/hooks/block-showcase-shortcode.php index 73f996ca..0ed8dc62 100644 --- a/inc/hooks/block-showcase-shortcode.php +++ b/inc/hooks/block-showcase-shortcode.php @@ -49,7 +49,8 @@ function get_block_showcase_preview_allowed_html() { 'class' => true, 'style' => true, ); - $allowed_html['input'] = array( + // Post context has no ; keep a broad list for search and other form blocks. + $allowed_html['input'] = array( 'type' => true, 'name' => true, 'value' => true, @@ -57,25 +58,46 @@ function get_block_showcase_preview_allowed_html() { 'required' => true, 'id' => true, 'class' => true, + 'style' => true, + 'disabled' => true, + 'readonly' => true, + 'size' => true, + 'maxlength' => true, + 'min' => true, + 'max' => true, + 'step' => true, + 'pattern' => true, + 'autocomplete' => true, + 'tabindex' => true, 'aria-label' => true, 'aria-labelledby' => true, 'aria-describedby' => true, + 'aria-hidden' => true, + 'aria-expanded' => true, + 'aria-controls' => true, + 'data-*' => true, ); - $allowed_html['button'] = array( - 'type' => true, - 'class' => true, - 'aria-label' => true, - 'aria-labelledby' => true, - ); - $allowed_html['form'] = array( - 'action' => true, - 'method' => true, - 'class' => true, - 'role' => true, - ); - $allowed_html['label'] = array( - 'for' => true, - 'class' => true, + // Do not replace post defaults for