PluginProbe
Extendify / trunk
Extendify vtrunk
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/Agent/Controllers/WPController.php +395 -123 3.0.4 → trunk View file →
@@ -7,9 +7,14 @@
7 7 namespace Extendify\Agent\Controllers;
8 8
9 9 defined('ABSPATH') || die('No direct access.');
10 10
11 +use Extendify\Agent\PostBlockFinder;
12 +use Extendify\Agent\TemplatePartBlockFinder;
13 +use Extendify\Constants;
14 +use Extendify\Shared\Services\HeroDescription;
11 15 use Extendify\Shared\Services\Sanitizer;
16 +use Extendify\Shared\Services\SiteImages;
12 17
13 18 /**
14 19 * The controller for interacting with WordPress.
15 20 */
@@ -49,8 +54,12 @@
49 54 'onyx',
50 55 'glasgow',
51 56 'royal',
52 57 'obsidian',
58 + 'heath',
59 + 'signal',
60 + 'marzipan',
61 + 'butterscotch',
53 62 ];
54 63
55 64 /**
56 65 * Recursively filter an array to include only specified properties.
@@ -282,9 +291,10 @@
282 291 'lineHeight' => $typography['lineHeight'] ?? null,
283 292 'letterSpacing' => $typography['letterSpacing'] ?? null,
284 293 'fontStyle' => $typography['fontStyle'] ?? null,
285 294 'fontWeight' => $typography['fontWeight'] ?? null,
286 - 'textTransform' => $typography['textTransform'] ?? 'none',
295 + // Defaulting to 'none' would cancel the applied vibe's uppercase.
296 + 'textTransform' => $typography['textTransform'] ?? null,
287 297 ], function ($v) {
288 298 return $v !== null;
289 299 })
290 300 ];
@@ -291,8 +301,75 @@
291 301 }
292 302
293 303
294 304 /**
305 + * Where else the synced pattern or menu a composite blockId names is used
306 + *
307 + * @param \WP_REST_Request $request The REST API request object.
308 + * @return \WP_REST_Response
309 + */
310 + public static function getSharedBlockUsage(\WP_REST_Request $request)
311 + {
312 + $composite = TemplatePartBlockFinder::parseCompositeId(
313 + $request->get_param('blockId')
314 + );
315 + if (!$composite) {
316 + return new \WP_REST_Response(['scope' => 'block', 'pages' => []], 200);
317 + }
318 +
319 + $seen = [];
320 + $pages = [];
321 + $sitewide = self::collectRefUsage($composite['ref'], $seen, $pages);
322 +
323 + return new \WP_REST_Response([
324 + 'scope' => $sitewide ? 'site' : ($pages ? 'pages' : 'block'),
325 + 'pages' => array_values($pages),
326 + ], 200);
327 + }
328 +
329 + // A template or part renders it everywhere, so a page list only matters otherwise.
330 + private static function collectRefUsage(int $ref, array &$seen, array &$pages): bool
331 + {
332 + if (isset($seen[$ref])) {
333 + return false;
334 + }
335 + $seen[$ref] = true;
336 +
337 + $sitewide = false;
338 + foreach (self::postsReferencing($ref) as $row) {
339 + $id = (int) $row->ID;
340 + if (in_array($row->post_type, ['wp_template', 'wp_template_part'], true)) {
341 + $sitewide = true;
342 + continue;
343 + }
344 + // A pattern nested in a pattern inherits wherever the outer one lands.
345 + if ($row->post_type === 'wp_block') {
346 + $sitewide = self::collectRefUsage($id, $seen, $pages) || $sitewide;
347 + continue;
348 + }
349 + $pages[$id] = ['id' => $id, 'title' => \get_the_title($id)];
350 + }
351 +
352 + return $sitewide;
353 + }
354 +
355 + // The ref is literal text inside a block comment, which WP indexes nowhere.
356 + private static function postsReferencing(int $ref)
357 + {
358 + global $wpdb;
359 +
360 + $like = $wpdb->esc_like('"ref":' . $ref);
361 + return $wpdb->get_results($wpdb->prepare(
362 + "SELECT ID, post_type FROM {$wpdb->posts}
363 + WHERE post_status IN ('publish', 'private', 'draft')
364 + AND post_type IN ('post', 'page', 'wp_block', 'wp_template', 'wp_template_part')
365 + AND (post_content LIKE %s OR post_content LIKE %s)",
366 + '%' . $like . '}%',
367 + '%' . $like . ',%'
368 + ));
369 + }
370 +
371 + /**
295 372 * Get the HTML of a specific tagged block code
296 373 *
297 374 * @param \WP_REST_Request $request The REST API request object.
298 375 * @return \WP_REST_Response
@@ -298,69 +375,127 @@
298 375 * @return \WP_REST_Response
299 376 */
300 377 public static function getBlockCode(\WP_REST_Request $request)
301 378 {
302 - $blockId = (int) $request->get_param('blockId');
303 - $postId = (int) $request->get_param('postId');
379 + $rawId = $request->get_param('blockId');
380 + $partSlug = (string) $request->get_param('partSlug');
304 381
382 + $composite = TemplatePartBlockFinder::parseCompositeId($rawId);
383 + if ($composite) {
384 + return self::getRefPostBlockCode(
385 + $composite,
386 + (string) $rawId,
387 + $partSlug,
388 + (int) $request->get_param('postId')
389 + );
390 + }
391 +
392 + $blockId = (int) $rawId;
305 393 if ($blockId < 1) {
306 394 return new \WP_REST_Response(['error' => 'Invalid blockId'], 400);
307 395 }
308 396
397 + // A template-part source resolves the part and walks it with the shared
398 + // preorder finder, instead of the post path below.
399 + if ($partSlug !== '') {
400 + return self::getTemplatePartBlockCode($partSlug, $blockId);
401 + }
402 +
403 + $postId = (int) $request->get_param('postId');
309 404 $post = \get_post($postId);
310 405 if (!$post) {
311 406 return new \WP_REST_Response(['error' => 'Post not found'], 404);
312 407 }
313 408
314 - $ignored = ['core/query', 'core/post-template', 'core/post-content'];
409 + $found = PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
315 410
316 - $ast = array_values(array_filter(
317 - parse_blocks($post->post_content),
318 - static function ($b) {
319 - return is_array($b) && !empty($b['blockName']);
320 - }
321 - ));
411 + if (!$found || empty($found['block']['blockName'])) {
412 + return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
413 + }
322 414
323 - $seq = 0;
324 - $found = null;
415 + return self::blockCodeResponse($found['block'], ['postId' => $postId], $blockId);
416 + }
325 417
326 - $walk = function (array $list) use (&$walk, &$seq, $blockId, &$found, $ignored) {
327 - foreach ($list as $b) {
328 - $name = $b['blockName'] ?? null;
329 - if (!$name) {
330 - continue;
331 - }
418 + // One response shape for every source, so the client reads the same fields
419 + // whichever post the block turned out to live in.
420 + private static function blockCodeResponse(array $block, array $scope, $blockId)
421 + {
422 + return new \WP_REST_Response(array_merge($scope, [
423 + 'blockId' => $blockId,
424 + 'name' => $block['blockName'],
425 + 'attrs' => $block['attrs'] ?? (object)[],
426 + 'block' => serialize_blocks([$block]),
427 + ]), 200);
428 + }
332 429
333 - // Ignore this block and its subtree (matches tagger behavior)
334 - if (in_array($name, $ignored, true)) {
335 - continue; // do NOT increment seq, do NOT traverse children
336 - }
430 + // Resolves the part the same way SaveController does — active-theme-scoped
431 + // get_block_template, not a raw get_posts by name — then walks it with the
432 + // shared preorder finder so the blockId lands on the block save will write.
433 + private static function getTemplatePartBlockCode(string $slug, int $blockId)
434 + {
435 + $content = self::templatePartContent($slug);
436 + if ($content === null) {
437 + return new \WP_REST_Response(['error' => 'Template part not found'], 404);
438 + }
337 439
338 - $seq++;
339 - if ($seq === $blockId) {
340 - $found = $b;
341 - return true;
342 - }
440 + $found = TemplatePartBlockFinder::find(
441 + parse_blocks($content),
442 + $blockId
443 + );
444 + if (!is_array($found) || empty($found['block']['blockName'])) {
445 + return new \WP_REST_Response(['error' => 'Block id not found in this template part'], 404);
446 + }
343 447
344 - if (!empty($b['innerBlocks']) && $walk($b['innerBlocks'])) {
345 - return true;
346 - }
347 - }
348 - return false;
349 - };
350 - $walk($ast);
448 + return self::blockCodeResponse($found['block'], ['partSlug' => $slug], $blockId);
449 + }
351 450
352 - if (!is_array($found) || empty($found['blockName'])) {
451 + // Post and theme file number identically, so an id survives the first edit.
452 + private static function templatePartContent(string $slug)
453 + {
454 + $stylesheet = \wp_get_theme()->get_stylesheet();
455 + $template = \get_block_template("{$stylesheet}//{$slug}", 'wp_template_part');
456 + if (!$template) {
457 + return null;
458 + }
459 + $post = empty($template->wp_id) ? null : \get_post($template->wp_id);
460 + return $post ? $post->post_content : $template->content;
461 + }
462 +
463 + // A synced pattern's or menu's blocks live in the post its id names, walked
464 + // with the same finder — the part they render inside owns none of them.
465 + private static function getRefPostBlockCode(
466 + array $composite,
467 + string $blockId,
468 + string $partSlug,
469 + int $postId
470 + ) {
471 + $post = \get_post($composite['ref']);
472 + if (!$post || $post->post_type !== $composite['postType']) {
473 + return new \WP_REST_Response(['error' => 'Referenced post not found'], 404);
474 + }
475 +
476 + $containerPost = $partSlug === '' ? \get_post($postId) : null;
477 + $container = $partSlug !== ''
478 + ? self::templatePartContent($partSlug)
479 + : ($containerPost ? $containerPost->post_content : null);
480 + if ($container === null) {
353 481 return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
354 482 }
355 483
356 - return new \WP_REST_Response([
357 - 'postId' => $postId,
358 - 'blockId' => $blockId,
359 - 'name' => $found['blockName'],
360 - 'attrs' => $found['attrs'] ?? (object)[],
361 - 'block' => serialize_blocks([$found]),
362 - ], 200);
484 + // Unchecked, every pattern and menu on the site reads back from any page.
485 + if (!TemplatePartBlockFinder::contentRendersRef($container, $composite['ref'])) {
486 + return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
487 + }
488 +
489 + $found = TemplatePartBlockFinder::find(
490 + parse_blocks($post->post_content),
491 + $composite['seq']
492 + );
493 + if (!is_array($found) || empty($found['block']['blockName'])) {
494 + return new \WP_REST_Response(['error' => 'Block id not found in this referenced post'], 404);
495 + }
496 +
497 + return self::blockCodeResponse($found['block'], ['partSlug' => $partSlug], $blockId);
363 498 }
364 499
365 500 /**
366 501 * Get the rendered HTML of some block code
@@ -372,9 +507,15 @@
372 507 {
373 508 $blockCode = $request->get_param('blockCode');
374 509 $content = \do_blocks($blockCode);
375 510
376 - return new \WP_REST_Response(['content' => trim($content)]);
511 + // Layout supports register per-container CSS for a page-side enqueue
512 + // that never happens on a REST fragment — ship it with the markup.
513 + $styles = function_exists('wp_style_engine_get_stylesheet_from_context')
514 + ? \wp_style_engine_get_stylesheet_from_context('block-supports')
515 + : '';
516 +
517 + return new \WP_REST_Response(['content' => trim($content), 'styles' => $styles]);
377 518 }
378 519
379 520 /**
380 521 * Get the Hero Patterns from the API
@@ -382,15 +523,22 @@
382 523 * @param string $title The title to replace in the pattern code
383 524 * @param string $description The description to replace in the pattern code
384 525 * @param array $images The images to replace in the pattern code, as an array of urls
385 526 * @param array $cta The cta to replace in the pattern code, as an array with 'label' and 'link' keys
386 - * @param bool $featured Whether to limit to featured patterns
387 - * @return array|WP_Error|array<string|int, mixed> The hero patterns data or a WP_Error on failure
527 + * @param bool $featuredOnly Whether to limit to featured patterns
528 + * @param string|null $source What triggered the request, e.g. 'change-site-design-workflow'
529 + * @return array|\WP_Error|array<string|int, mixed> The hero patterns data or a WP_Error on failure
388 530 */
389 - protected static function getHeroPatternsData($title, $description, $images, $cta, $featuredOnly = false)
390 - {
531 + protected static function getHeroPatternsData(
532 + $title,
533 + $description,
534 + $images,
535 + $cta,
536 + $featuredOnly = false,
537 + $source = null
538 + ) {
391 539 $response = \wp_remote_post(
392 - 'https://patterns.extendify.com/api/heros',
540 + Constants::PATTERNS_HOST . '/api/heros',
393 541 [
394 542 'headers' => [
395 543 'Content-Type' => 'application/json',
396 544 'Accept' => 'application/json',
@@ -398,8 +546,9 @@
398 546 'body' => wp_json_encode([
399 547 "wpVersion" => \get_bloginfo('version'),
400 548 "wpLanguage" => \get_locale(),
401 549 "featured" => $featuredOnly,
550 + "source" => $source,
402 551 ])
403 552 ]
404 553 );
405 554
@@ -408,93 +557,104 @@
408 557 }
409 558
410 559 $body = json_decode(\wp_remote_retrieve_body($response), true);
411 560
412 - $heroPatterns = array_map(
413 - function ($heroPattern) use ($description, $title, $images, $cta) {
414 - $code = $heroPattern['code'] ?? '';
561 + $cursor = 0;
562 + $imageCount = count($images);
563 + $heroPatterns = [];
415 564
416 - if ($title) {
417 - $code = preg_replace(
418 - '/(<!-- wp:heading[^>]*-->[\s\S]*?<h1[^>]*>)[\s\S]*?(<\/h1>[\s\S]*?<!-- \/wp:heading -->)/m',
419 - '${1}' . esc_html($title) . '${2}',
420 - $code,
421 - 1
422 - );
423 - }
565 + foreach ($body as $heroPattern) {
566 + $code = $heroPattern['code'] ?? '';
424 567
425 - if ($description) {
426 - $code = preg_replace(
427 - '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
428 - '${1}' . esc_html($description) . '${2}',
429 - $code,
430 - 1
431 - );
432 - }
568 + if ($title) {
569 + $code = preg_replace(
570 + '/(<!-- wp:heading[^>]*-->[\s\S]*?<h1[^>]*>)[\s\S]*?(<\/h1>[\s\S]*?<!-- \/wp:heading -->)/m',
571 + '${1}' . esc_html($title) . '${2}',
572 + $code,
573 + 1
574 + );
575 + }
433 576
434 - if ($cta['label'] ?? null) {
435 - $code = preg_replace(
436 - '/(<!-- wp:button[^>]*-->[\s\S]*?<a[^>]*>)[\s\S]*?(<\/a>[\s\S]*?<!-- \/wp:button -->)/m',
437 - '${1}' . esc_html($cta['label']) . '${2}',
438 - $code,
439 - 1
440 - );
441 - }
577 + if (trim((string) $description) === '') {
578 + // Catalog paragraphs describe the slot, not the site.
579 + $code = preg_replace(
580 + '/<!-- wp:paragraph[\s\S]*?<!-- \/wp:paragraph -->/m',
581 + '',
582 + $code,
583 + 1
584 + );
585 + } else {
586 + $code = preg_replace(
587 + '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
588 + '${1}' . esc_html($description) . '${2}',
589 + $code,
590 + 1
591 + );
592 + }
442 593
443 - if ($cta['link'] ?? null) {
444 - $code = preg_replace(
445 - '/(<!-- wp:button[\s\S]*?<a[^>]*\shref=")[^"]*(")/m',
446 - '${1}' . esc_url($cta['link']) . '${2}',
447 - $code,
448 - 1
449 - );
450 - }
594 + if ($cta['label'] ?? null) {
595 + $code = preg_replace(
596 + '/(<!-- wp:button[^>]*-->[\s\S]*?<a[^>]*>)[\s\S]*?(<\/a>[\s\S]*?<!-- \/wp:button -->)/m',
597 + '${1}' . esc_html($cta['label']) . '${2}',
598 + $code,
599 + 1
600 + );
601 + }
451 602
452 - foreach ($heroPattern['urls'] ?? [] as $key => $url) {
453 - if (!($images[$key] ?? null)) {
454 - break;
455 - }
603 + if ($cta['link'] ?? null) {
604 + $code = preg_replace(
605 + '/(<!-- wp:button[\s\S]*?<a[^>]*\shref=")[^"]*(")/m',
606 + '${1}' . esc_url($cta['link']) . '${2}',
607 + $code,
608 + 1
609 + );
610 + }
456 611
457 - $code = str_replace($url, $images[$key], $code);
612 + $patternUrls = $heroPattern['urls'] ?? [];
613 + foreach ($patternUrls as $key => $url) {
614 + if (!$imageCount) {
615 + break;
458 616 }
617 + $code = str_replace($url, $images[($cursor + $key) % $imageCount], $code);
618 + }
619 + $cursor = $imageCount ? ($cursor + count($patternUrls)) % $imageCount : 0;
459 620
460 - $renderedHtml = do_blocks(str_replace('ext-animate--on', '', $code));
621 + $renderedHtml = do_blocks(str_replace('ext-animate--on', '', $code));
461 622
462 - $blockSupportsCss = function_exists('wp_style_engine_get_stylesheet_from_context')
463 - ? wp_style_engine_get_stylesheet_from_context('block-supports')
464 - : '';
623 + $blockSupportsCss = function_exists('wp_style_engine_get_stylesheet_from_context')
624 + ? wp_style_engine_get_stylesheet_from_context('block-supports')
625 + : '';
465 626
466 - // Clear block-supports store so CSS doesn't accumulate across patterns.
467 - \WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
627 + // Clear block-supports store so CSS doesn't accumulate across patterns.
628 + \WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
468 629
469 - $linkStyles = array_values(
470 - array_filter(
471 - array_map(
472 - function ($style) {
473 - return wp_styles()->registered[$style]->src ?? null;
474 - },
475 - wp_styles()->queue ?? []
476 - )
630 + $linkStyles = array_values(
631 + array_filter(
632 + array_map(
633 + function ($style) {
634 + return wp_styles()->registered[$style]->src ?? null;
635 + },
636 + wp_styles()->queue ?? []
477 637 )
478 - );
638 + )
639 + );
479 640
480 - /**
481 - * Clear queue for the next pattern.
482 - *
483 - * `do_blocks` appends to the global queue the styles needed for the blocks.
484 - */
485 - wp_styles()->queue = [];
641 + /**
642 + * Clear queue for the next pattern.
643 + *
644 + * `do_blocks` appends to the global queue the styles needed for the blocks.
645 + */
646 + wp_styles()->queue = [];
486 647
487 - return [
488 - 'id' => $heroPattern['id'],
489 - 'code' => $code,
490 - 'renderedHtml' => $renderedHtml,
491 - 'blockSupportsCss' => $blockSupportsCss,
492 - 'linkStyles' => $linkStyles,
493 - ];
494 - },
495 - $body
496 - );
648 + $heroPatterns[] = [
649 + 'id' => $heroPattern['id'],
650 + 'name' => $heroPattern['name'],
651 + 'code' => $code,
652 + 'renderedHtml' => $renderedHtml,
653 + 'blockSupportsCss' => $blockSupportsCss,
654 + 'linkStyles' => $linkStyles,
655 + ];
656 + }
497 657
498 658 return $heroPatterns;
499 659 }
500 660
@@ -522,28 +682,140 @@
522 682
523 683 return new \WP_REST_Response(['patterns' => $heroPatterns, 'blockEditorSettings' => $editorSettings ?? null]);
524 684 }
525 685
686 + /**
687 + * Build the image list for the hero section.
688 + *
689 + * Returns up to 6 images: unused site images first, already-used images last.
690 + * When already at capacity, reverses the array so the last-used image leads next time.
691 + */
692 + protected static function getHeroSectionImages(array $images, array $siteImages, int $postId): array
693 + {
694 + $maxSlots = 6;
695 +
696 + if (count($images) >= $maxSlots) {
697 + return array_reverse($images);
698 + }
699 +
700 + $candidates = SiteImages::urls($siteImages);
701 +
702 + if (!$postId || empty($candidates)) {
703 + return $images;
704 + }
705 +
706 + $usedImages = self::resolveUsedImages($postId);
707 + $candidates = array_map(function ($url) {
708 + return self::stripQueryString($url);
709 + }, $candidates);
710 +
711 + $unusedImages = array_values(array_filter(
712 + $candidates,
713 + function ($url) use ($usedImages, $images) {
714 + return !in_array($url, $usedImages, true) && !in_array($url, $images, true);
715 + }
716 + ));
717 +
718 + $unusedSlots = $maxSlots - count($images);
719 + return array_merge(array_slice($unusedImages, 0, $unusedSlots), $images);
720 + }
721 +
722 + protected static function stripQueryString(string $url): string
723 + {
724 + $parsed = wp_parse_url($url);
725 + return ($parsed['scheme'] ?? 'https') . '://' . ($parsed['host'] ?? '') . ($parsed['path'] ?? '');
726 + }
727 +
728 + /**
729 + * Get all Unsplash URLs used in a post.
730 + */
731 + protected static function resolveUsedImages(int $postId): array
732 + {
733 + $post = get_post($postId);
734 + if (!$post) {
735 + return [];
736 + }
737 +
738 + $processor = new \WP_HTML_Tag_Processor($post->post_content);
739 + $usedImages = [];
740 +
741 + while ($processor->next_tag('img')) {
742 + $src = $processor->get_attribute('src');
743 + if (!$src) {
744 + continue;
745 + }
746 +
747 + $baseUrl = self::stripQueryString($src);
748 +
749 + if (str_contains($src, 'unsplash.com')) {
750 + $usedImages[] = $baseUrl;
751 + continue;
752 + }
753 +
754 + $attachmentId = attachment_url_to_postid(esc_url($baseUrl));
755 +
756 + if (!$attachmentId) {
757 + continue;
758 + }
759 +
760 + $sourceUrl = get_post_meta($attachmentId, '_extendify_source_url', true);
761 +
762 + if (!$sourceUrl) {
763 + continue;
764 + }
765 +
766 + $usedImages[] = self::stripQueryString($sourceUrl);
767 + }
768 +
769 + return array_values(array_unique($usedImages));
770 + }
771 +
526 772 public static function getSiteDesignVariations(\WP_REST_Request $request)
527 773 {
528 774 $title = $request->get_param('title');
529 - $description = $request->get_param('description');
530 - $images = $request->get_param('images');
775 + $description = HeroDescription::resolve($request->get_param('description'));
776 + $images = $request->get_param('images') ?? [];
531 777 $cta = $request->get_param('cta');
532 778 $featuredOnly = true; // Only show featured patterns
779 + $currentHeroPattern = $request->get_param('currentHeroPattern');
780 + $postId = (int) $request->get_param('postId');
781 + $siteImages = $request->get_param('siteImages') ?? [];
782 + $source = $request->get_param('source');
533 783
534 - $heroPatterns = self::getHeroPatternsData($title, $description, $images, $cta, $featuredOnly);
784 + $images = self::getHeroSectionImages($images, $siteImages, $postId);
535 785
786 + $heroPatterns = self::getHeroPatternsData($title, $description, $images, $cta, $featuredOnly, $source);
787 +
536 788 if (\is_wp_error($heroPatterns)) {
537 789 return new \WP_REST_Response([], 500);
538 790 }
539 791
792 + $heroPatterns = array_values(
793 + array_filter(
794 + $heroPatterns,
795 + function ($heroPattern) use ($currentHeroPattern) {
796 + if (!$currentHeroPattern) {
797 + return true;
798 + }
799 +
800 + return $heroPattern['name'] !== $currentHeroPattern;
801 + }
802 + )
803 + );
804 +
540 805 $current = \WP_Theme_JSON_Resolver::get_merged_data('theme');
541 806
542 807 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
543 808
809 + // Keep only full style variations — exclude color-only and font-only
810 + // presets that get_style_variations() returns from styles/colors/* and
811 + // styles/typography/*.
544 812 $colorAndFontsVariations = array_filter($unfiltered, function ($variation) {
545 - return self::variationHasProperties($variation, ['color', 'elements', 'typography']);
813 + $hasPalette = ($variation['settings']['color']['palette'] ?? []) !== [];
814 + $hasTypography = ($variation['styles']['typography'] ?? []) !== []
815 + || ($variation['settings']['typography'] ?? []) !== [];
816 + $hasElements = ($variation['styles']['elements'] ?? []) !== [];
817 + return $hasPalette && $hasTypography && $hasElements;
546 818 });
547 819
548 820 $buildSlugMap = function ($unfiltered) {
549 821 $slugMap = [];