From 70f682aa8f9b7f6b825016a73ccfc42c35206a98 Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:38 +0100 Subject: [PATCH 01/13] Ipsum: Address the review notes from the 7.2 Beta 5 core commit. These follow up on the review of the 7.2 Beta 5 sync. The front end picks `style.min.css` with a plain `SCRIPT_DEBUG` check, as Twenty Twenty-Five does, instead of comparing file times, and `style.css` gets Twenty Twenty-Five's note against editing it in the Theme File Editor. The template descriptions now say "the theme header" and "the theme footer", and the `ipsum_get_design_guide_presets()` docblock says it returns an empty list when the presets aren't an array. Synced from https://github.com/WordPress/ipsum/pull/216. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/functions.php | 20 +++---------------- src/wp-content/themes/ipsum/inc/abilities.php | 4 +++- .../themes/ipsum/patterns/hidden-404.php | 2 +- .../ipsum/patterns/hidden-archive-sidebar.php | 2 +- .../themes/ipsum/patterns/hidden-archive.php | 2 +- .../ipsum/patterns/hidden-index-sidebar.php | 2 +- .../themes/ipsum/patterns/hidden-index.php | 2 +- .../ipsum/patterns/hidden-page-no-title.php | 2 +- .../ipsum/patterns/hidden-page-sidebar.php | 2 +- .../themes/ipsum/patterns/hidden-page.php | 2 +- .../ipsum/patterns/hidden-search-bar.php | 2 +- .../themes/ipsum/patterns/hidden-search.php | 2 +- .../ipsum/patterns/hidden-single-sidebar.php | 2 +- .../themes/ipsum/patterns/hidden-single.php | 2 +- src/wp-content/themes/ipsum/style.css | 7 +++++++ 15 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/wp-content/themes/ipsum/functions.php b/src/wp-content/themes/ipsum/functions.php index 33c403d9f9608..eab1a8be0e4fa 100644 --- a/src/wp-content/themes/ipsum/functions.php +++ b/src/wp-content/themes/ipsum/functions.php @@ -48,23 +48,9 @@ function ipsum_add_theme_support() { * @return void */ function ipsum_enqueue_styles() { - $style_path = get_parent_theme_file_path( 'style.css' ); - $style_min_path = get_parent_theme_file_path( 'style.min.css' ); - - /* - * Use the minified stylesheet, unless script debugging is on or style.css - * has changed since style.min.css was built. That keeps edits made in the - * Theme File Editor working: saving style.css makes it the newer file. - */ - if ( - SCRIPT_DEBUG - || ! file_exists( $style_min_path ) - || ( filemtime( $style_path ) > filemtime( $style_min_path ) ) - ) { - $src = 'style.css'; - } else { - $src = 'style.min.css'; - } + // Use the minified stylesheet unless script debugging is on, as Twenty Twenty-Five does. + $suffix = SCRIPT_DEBUG ? '' : '.min'; + $src = 'style' . $suffix . '.css'; // Register theme stylesheet. wp_register_style( diff --git a/src/wp-content/themes/ipsum/inc/abilities.php b/src/wp-content/themes/ipsum/inc/abilities.php index 0257ae2c3b526..c5b6a6469e4c8 100644 --- a/src/wp-content/themes/ipsum/inc/abilities.php +++ b/src/wp-content/themes/ipsum/inc/abilities.php @@ -260,8 +260,10 @@ function ipsum_get_design_guide_schema() { * @since Ipsum 1.0 * * @param mixed $presets The presets from the global settings, keyed by origin. + * Anything other than an array gives an empty list. * @param string[] $keys The preset properties to keep. - * @return array> The presets, with only the given properties. + * @return array> The presets, with only the given properties. Empty when + * `$presets` isn't an array. */ function ipsum_get_design_guide_presets( $presets, $keys ) { $items = array(); diff --git a/src/wp-content/themes/ipsum/patterns/hidden-404.php b/src/wp-content/themes/ipsum/patterns/hidden-404.php index 712868ca5e4a0..368d9378aeb99 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-404.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-404.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-404 * Template Types: 404 * Inserter: no - * Description: The 404 template. It has the header, a “Page not found.” heading and a short note over the search field, and the footer. + * Description: The 404 template. It has the theme header, a “Page not found.” heading and a short note over the search field, and the theme footer. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php b/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php index 3cc079683c608..7700746c58f9a 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-archive-sidebar * Template Types: archive, category, tag * Inserter: no - * Description: The All Archives Sidebar template. It has the archive title and the Archive Simple listing beside the Sidebar part on the wide width, between the header and the wide footer, with the search bar pinned to the bottom. + * Description: The All Archives Sidebar template. It has the archive title and the Archive Simple listing beside the Sidebar part on the wide width, between the theme header and the wide theme footer, with the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-archive.php b/src/wp-content/themes/ipsum/patterns/hidden-archive.php index b2bb1c7ebf24a..b6cc192e3c2bb 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-archive.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-archive.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-archive * Template Types: archive, category, tag * Inserter: no - * Description: The All Archives template for categories, tags, authors and dates. It has the header, the archive title over the Archive Compact listing, the footer, and the search bar pinned to the bottom. + * Description: The All Archives template for categories, tags, authors and dates. It has the theme header, the archive title over the Archive Compact listing, the theme footer, and the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-index-sidebar.php b/src/wp-content/themes/ipsum/patterns/hidden-index-sidebar.php index 0d474e5fa9f58..92cd7f7ce4d1b 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-index-sidebar.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-index-sidebar.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-index-sidebar * Template Types: index, home * Inserter: no - * Description: The Index Sidebar template. It has the latest posts in full, as in Archive Full, beside the Sidebar part on the wide width, between the header and the wide footer, with the search bar pinned to the bottom. + * Description: The Index Sidebar template. It has the latest posts in full, as in Archive Full, beside the Sidebar part on the wide width, between the theme header and the wide theme footer, with the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-index.php b/src/wp-content/themes/ipsum/patterns/hidden-index.php index 60a7002a3b229..7648a6b6ea5c9 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-index.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-index.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-index * Template Types: index, home * Inserter: no - * Description: The Index template, also the blog home. It has the header, the latest posts in the Archive Full listing with pagination, the footer, and the search bar pinned to the bottom. + * Description: The Index template, also the blog home. It has the theme header, the latest posts in the Archive Full listing with pagination, the theme footer, and the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-page-no-title.php b/src/wp-content/themes/ipsum/patterns/hidden-page-no-title.php index 4422546e06b1d..07891f858bc2e 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-page-no-title.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-page-no-title.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-page-no-title * Template Types: page * Inserter: no - * Description: The Pages No Title template, for pages built from patterns. It has the header, the page content with no title or featured image, the footer, and the search bar pinned to the bottom. + * Description: The Pages No Title template, for pages built from patterns. It has the theme header, the page content with no title or featured image, the theme footer, and the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-page-sidebar.php b/src/wp-content/themes/ipsum/patterns/hidden-page-sidebar.php index f4627cbac30c9..278095cffb46d 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-page-sidebar.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-page-sidebar.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-page-sidebar * Template Types: page * Inserter: no - * Description: The Pages Sidebar template. It has the featured image, title and content beside the Sidebar part on the wide width, between the header and the wide footer, with the search bar pinned to the bottom. + * Description: The Pages Sidebar template. It has the featured image, title and content beside the Sidebar part on the wide width, between the theme header and the wide theme footer, with the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-page.php b/src/wp-content/themes/ipsum/patterns/hidden-page.php index 35035c4e2ff8b..3af59b56ff7f2 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-page.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-page.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-page * Template Types: page * Inserter: no - * Description: The Pages template. It has the header, the featured image, title and content, the footer, and the search bar pinned to the bottom. + * Description: The Pages template. It has the theme header, the featured image, title and content, the theme footer, and the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-search-bar.php b/src/wp-content/themes/ipsum/patterns/hidden-search-bar.php index 9069e7c7016e1..e060867930c7e 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-search-bar.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-search-bar.php @@ -3,7 +3,7 @@ * Title: Search Bar * Slug: ipsum/hidden-search-bar * Inserter: no - * Description: The Search Bar template part. It has a search button on the right that opens into a field. Most templates place it after the footer and pin it to the bottom of the screen. + * Description: The Search Bar template part. It has a search button on the right that opens into a field. Most templates place it after the theme footer and pin it to the bottom of the screen. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-search.php b/src/wp-content/themes/ipsum/patterns/hidden-search.php index 66b8c239fb025..93f9a9a268903 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-search.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-search.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-search * Template Types: search * Inserter: no - * Description: The Search Results template. It has the header, the results title over the search field, the matching posts in the Archive Compact listing with a no-results message and pagination, and the footer. + * Description: The Search Results template. It has the theme header, the results title over the search field, the matching posts in the Archive Compact listing with a no-results message and pagination, and the theme footer. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-single-sidebar.php b/src/wp-content/themes/ipsum/patterns/hidden-single-sidebar.php index 7a7c55d8e1ff3..893cb5e18cdf8 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-single-sidebar.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-single-sidebar.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-single-sidebar * Template Types: single * Inserter: no - * Description: The Single Posts Sidebar template. It has the post with its tags, author card, Comments and Keep Reading sections beside the Sidebar part on the wide width, between the header and the wide footer, with the search bar pinned to the bottom. + * Description: The Single Posts Sidebar template. It has the post with its tags, author card, Comments and Keep Reading sections beside the Sidebar part on the wide width, between the theme header and the wide theme footer, with the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/patterns/hidden-single.php b/src/wp-content/themes/ipsum/patterns/hidden-single.php index f914fc8f5425a..6b66b10118656 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-single.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-single.php @@ -4,7 +4,7 @@ * Slug: ipsum/hidden-single * Template Types: single * Inserter: no - * Description: The Single Posts template. It has the header, the featured image, title, date and categories, content, tags and author card, then the Comments and Keep Reading sections, the footer, and the search bar pinned to the bottom. + * Description: The Single Posts template. It has the theme header, the featured image, title, date and categories, content, tags and author card, then the Comments and Keep Reading sections, the theme footer, and the search bar pinned to the bottom. * * @package Ipsum * @since Ipsum 1.0 diff --git a/src/wp-content/themes/ipsum/style.css b/src/wp-content/themes/ipsum/style.css index 46d62a2c944df..b9ad4abb6208c 100644 --- a/src/wp-content/themes/ipsum/style.css +++ b/src/wp-content/themes/ipsum/style.css @@ -14,6 +14,13 @@ Text Domain: ipsum Tags: one-column, featured-images, full-site-editing, block-patterns, rtl-language-support, sticky-post, threaded-comments, translation-ready, wide-blocks, block-styles, style-variations, blog */ +/* + * IMPORTANT: This file is only served on the frontend when `SCRIPT_DEBUG` is enabled; + * in most instances, the `style.min.css` file will be served. It is not recommended that you + * use the Theme File Editor to modify this stylesheet. Instead, add the necessary style + * overrides via "Additional CSS" in the Site Editor. + */ + /* Crossfade page transition */ @media (prefers-reduced-motion: no-preference) { From 6e7e23ae59f910eda9ca2577ca71d95354ffe231 Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:38 +0100 Subject: [PATCH 02/13] Ipsum: Fix Pullquote custom line height not applying on the front end. The theme's paragraph line height targeted the `p` directly, so it beat a custom line height set on a Pullquote, Quote or Group. The `1.6` line height now sits on the root instead, like in Twenty Twenty-Four and Twenty Twenty-Five, so a custom line height reaches the paragraphs inside. Lists, tables, code and citations go from `1.5` to `1.6`. Synced from https://github.com/WordPress/ipsum/pull/203. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/theme.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-content/themes/ipsum/theme.json b/src/wp-content/themes/ipsum/theme.json index cdc8fd86d446d..05ecc05dca107 100644 --- a/src/wp-content/themes/ipsum/theme.json +++ b/src/wp-content/themes/ipsum/theme.json @@ -816,11 +816,6 @@ "fontWeight": "500" } }, - "core/paragraph": { - "typography": { - "lineHeight": "1.6" - } - }, "core/post-author": { "css": "& .wp-block-post-author__bio{font-size:inherit;margin:0;} & .wp-block-post-author__byline{font-size:inherit;}" }, @@ -1326,7 +1321,7 @@ "fontFamily": "var(--wp--preset--font-family--inter)", "fontSize": "var(--wp--preset--font-size--medium)", "fontWeight": "400", - "lineHeight": "1.5" + "lineHeight": "1.6" } }, "templateParts": [ From faa7e23d5c6637a97a8767188404888d7ad226ab Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:39 +0100 Subject: [PATCH 03/13] Ipsum: Fix horizontal scroll caused by long unbroken words in comments. A comment or comment author name with a long string and no spaces made the whole page scroll sideways. Comment content and author names now use `overflow-wrap: anywhere`, so they wrap inside the comment box. Synced from https://github.com/WordPress/ipsum/pull/200. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/patterns/hidden-comments.php | 4 ++-- src/wp-content/themes/ipsum/style.css | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/wp-content/themes/ipsum/patterns/hidden-comments.php b/src/wp-content/themes/ipsum/patterns/hidden-comments.php index 71ba6895c0178..5d1adbc6f8ce6 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-comments.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-comments.php @@ -20,8 +20,8 @@ - -
+ +
diff --git a/src/wp-content/themes/ipsum/style.css b/src/wp-content/themes/ipsum/style.css index b9ad4abb6208c..2cad46c508b94 100644 --- a/src/wp-content/themes/ipsum/style.css +++ b/src/wp-content/themes/ipsum/style.css @@ -187,6 +187,15 @@ footer.wp-block-template-part { display: none; } +/* + * Long unbroken author names still grow their flex row (avatar + meta). + * Comment content inherits body's break-word once the template wrapper is a + * plain group; only the author needs anywhere so min-content can shrink. + */ +.wp-block-comment-author-name { + overflow-wrap: anywhere; +} + /* * Sticky headers: a sticky root block inside a template part has no room to * travel — the part's wrapper element is exactly as tall as its content — so From e48a0b459aa03d1e6a153e6ccc56a96246cb4630 Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:39 +0100 Subject: [PATCH 04/13] Ipsum: Improve design guide defaults and schema descriptions. The `ipsum/get-design-guide` ability now returns only the basic design sections by default, and the larger lists, such as patterns and templates, are available through `fields`. Its schema gets clearer translated titles and descriptions, and marks the nested fields that are always there. The tests live in core, since Ipsum's repository has no PHPUnit setup. They cover the ability's registration and schema, the default and requested sections, invalid input, permissions, the REST endpoint, and how the guide follows the saved global styles and customized templates. Synced from https://github.com/WordPress/ipsum/pull/193. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/inc/abilities.php | 485 ++++++++++++++---- .../theme/ipsumGetDesignGuideAbility.php | 427 +++++++++++++++ 2 files changed, 824 insertions(+), 88 deletions(-) create mode 100644 tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php diff --git a/src/wp-content/themes/ipsum/inc/abilities.php b/src/wp-content/themes/ipsum/inc/abilities.php index c5b6a6469e4c8..8811e7186b008 100644 --- a/src/wp-content/themes/ipsum/inc/abilities.php +++ b/src/wp-content/themes/ipsum/inc/abilities.php @@ -27,18 +27,19 @@ function ipsum_register_abilities() { 'ipsum/get-design-guide', array( 'label' => __( 'Get Design Guide', 'ipsum' ), - 'description' => __( 'Returns the design guide of the Ipsum theme, read from the site as it is now: the color palette and what each color is for, whether it is light or dark, fonts, font sizes, spacing, layout widths, style variations, block styles, patterns, and the templates and template parts with the pattern that fills each. Read it before building pages, templates or styles. Pass fields to get only some sections.', 'ipsum' ), + 'description' => __( 'Returns the design guide of the Ipsum theme, read from the site as it is now. By default returns the basic design sections: the active theme, whether the palette is light or dark, the color palette and what each color is for, gradients, duotone, typography, spacing, layout widths, and guidelines. Pass fields to request specific sections, including larger lists such as style variations, block styles, patterns, templates, template parts, and template part areas. Read it before building pages, templates or styles.', 'ipsum' ), 'category' => 'site', 'input_schema' => array( 'type' => 'object', 'properties' => array( 'fields' => array( 'type' => 'array', + 'title' => __( 'Fields', 'ipsum' ), + 'description' => __( 'Optional. Sections to return. When omitted or empty, returns the basic design sections (theme, colorScheme, colors, gradients, duotone, typography, spacing, layout, guidelines). When set, returns only the listed sections. Every section name in the enum is valid, including larger lists such as patterns, templates, template parts, template part areas, block styles, and style variations.', 'ipsum' ), 'items' => array( 'type' => 'string', 'enum' => array_keys( $output_schema['properties'] ), ), - 'description' => __( 'Optional. Limits the guide to these sections. All sections are returned when it is omitted.', 'ipsum' ), ), ), 'additionalProperties' => false, @@ -84,10 +85,41 @@ function ipsum_get_color_roles() { } endif; +if ( ! function_exists( 'ipsum_get_design_guide_default_fields' ) ) : + /** + * Returns the design guide sections included when fields is omitted or empty. + * + * Larger lists (style variations, block styles, patterns, templates, + * template parts, and template part areas) are left out of the default + * response and must be requested through fields. + * + * @since Ipsum 1.0 + * + * @return string[] Section names. + */ + function ipsum_get_design_guide_default_fields() { + return array( + 'theme', + 'colorScheme', + 'colors', + 'gradients', + 'duotone', + 'typography', + 'spacing', + 'layout', + 'guidelines', + ); + } +endif; + if ( ! function_exists( 'ipsum_get_design_guide_schema' ) ) : /** * Returns the JSON Schema of the design guide, with one property per section. * + * Top-level sections stay optional so callers can request a subset. Nested + * objects with a fixed set of fields mark guaranteed properties as required + * and set additionalProperties to false. + * * @since Ipsum 1.0 * * @return array The output schema of the design guide ability. @@ -98,155 +130,428 @@ function ipsum_get_color_roles() { * } */ function ipsum_get_design_guide_schema() { - $string = array( 'type' => 'string' ); - $nullable = array( 'type' => array( 'string', 'null' ) ); - $strings = array( + $string = array( 'type' => 'string' ); + $nullable = array( + 'type' => array( 'string', 'null' ), + 'description' => __( 'A font family preset slug, a literal font-family value, or null when unset.', 'ipsum' ), + ); + $strings = array( 'type' => 'array', 'items' => $string, ); - $scheme = array( - 'type' => 'string', - 'enum' => array( 'light', 'dark' ), + $scheme = array( + 'type' => 'string', + 'title' => __( 'Color Scheme', 'ipsum' ), + 'description' => __( 'Whether the palette is light or dark.', 'ipsum' ), + 'enum' => array( 'light', 'dark' ), ); - $object_list = static function ( $description, $properties ) { + + $object_list = static function ( $title, $description, $properties, $required = array() ) { + $items = array( + 'type' => 'object', + 'properties' => $properties, + 'additionalProperties' => false, + ); + if ( ! empty( $required ) ) { + $items['required'] = $required; + } return array( 'type' => 'array', + 'title' => $title, 'description' => $description, - 'items' => array( - 'type' => 'object', - 'properties' => $properties, - ), + 'items' => $items, ); }; - $preset_list = static function ( $description, $value_key, $value_schema ) use ( $object_list, $string ) { - return $object_list( - $description, + + $preset_list = static function ( $title, $description, $value_key, $value_schema, $required, $extra_properties = array() ) use ( $object_list ) { + $name_description = in_array( 'name', $required, true ) + ? __( 'The human-readable preset name.', 'ipsum' ) + : __( 'The human-readable preset name. Omitted when the preset does not set one.', 'ipsum' ); + $properties = array_merge( array( - 'slug' => $string, - 'name' => $string, + 'slug' => array( + 'type' => 'string', + 'title' => __( 'Slug', 'ipsum' ), + 'description' => __( 'The preset slug.', 'ipsum' ), + ), + 'name' => array( + 'type' => 'string', + 'title' => __( 'Name', 'ipsum' ), + 'description' => $name_description, + ), $value_key => $value_schema, - ) + ), + $extra_properties ); + return $object_list( $title, $description, $properties, $required ); }; - $colors = $preset_list( __( 'The color palette, with what each color is for. Every style variation redefines the same slugs. Colors added on the site have no role.', 'ipsum' ), 'color', $string ); + $colors = $preset_list( + __( 'Colors', 'ipsum' ), + __( 'The color palette from the theme and any colors added on the site, with what each theme color is for. Every style variation redefines the same slugs. The role field is omitted for colors that have no known role.', 'ipsum' ), + 'color', + array( + 'type' => 'string', + 'title' => __( 'Color', 'ipsum' ), + 'description' => __( 'The CSS color value.', 'ipsum' ), + ), + array( 'slug', 'name', 'color' ), + array( + 'role' => array( + 'type' => 'string', + 'title' => __( 'Role', 'ipsum' ), + 'description' => __( 'What the color is for. Present only for theme palette colors with a known role.', 'ipsum' ), + ), + ) + ); - $colors['items']['properties']['role'] = $string; + $font_value = array( + 'type' => array( 'string', 'null' ), + 'description' => __( 'A font family preset slug, a literal font-family value, or null when unset. headingFont falls back to bodyFont when headings do not set their own family.', 'ipsum' ), + ); return array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'theme' => array( - 'type' => 'object', - 'description' => __( 'The active theme. Its stylesheet is the one to use in REST routes such as /wp/v2/global-styles/themes/{stylesheet}.', 'ipsum' ), - 'properties' => array( - 'name' => $string, - 'stylesheet' => $string, + 'type' => 'object', + 'title' => __( 'Theme', 'ipsum' ), + 'description' => __( 'The active theme. Its stylesheet is the one to use in REST routes such as /wp/v2/global-styles/themes/{stylesheet}.', 'ipsum' ), + 'properties' => array( + 'name' => array( + 'type' => 'string', + 'title' => __( 'Name', 'ipsum' ), + 'description' => __( 'The active theme name.', 'ipsum' ), + ), + 'stylesheet' => array( + 'type' => 'string', + 'title' => __( 'Stylesheet', 'ipsum' ), + 'description' => __( 'The theme stylesheet slug.', 'ipsum' ), + ), ), + 'required' => array( 'name', 'stylesheet' ), + 'additionalProperties' => false, ), 'colorScheme' => array_merge( $scheme, array( 'description' => __( 'Whether the active palette is light or dark.', 'ipsum' ) ) ), 'colors' => $colors, - 'gradients' => $preset_list( __( 'The gradient presets.', 'ipsum' ), 'gradient', $string ), - 'duotone' => $preset_list( __( 'The duotone filter presets.', 'ipsum' ), 'colors', $strings ), + 'gradients' => $preset_list( + __( 'Gradients', 'ipsum' ), + __( 'The gradient presets from the theme and any added on the site.', 'ipsum' ), + 'gradient', + array( + 'type' => 'string', + 'title' => __( 'Gradient', 'ipsum' ), + 'description' => __( 'The CSS gradient value.', 'ipsum' ), + ), + array( 'slug', 'name', 'gradient' ) + ), + 'duotone' => $preset_list( + __( 'Duotone', 'ipsum' ), + __( 'The duotone filter presets from the theme and any added on the site.', 'ipsum' ), + 'colors', + array_merge( + $strings, + array( + 'title' => __( 'Colors', 'ipsum' ), + 'description' => __( 'The two colors in the duotone filter.', 'ipsum' ), + ) + ), + array( 'slug', 'name', 'colors' ) + ), 'typography' => array( - 'type' => 'object', - 'description' => __( 'The font families of body text and headings, by slug, and the font family and font size presets.', 'ipsum' ), - 'properties' => array( - 'bodyFont' => $nullable, - 'headingFont' => $nullable, + 'type' => 'object', + 'title' => __( 'Typography', 'ipsum' ), + 'description' => __( 'The font families of body text and headings, the blocks that use another family, and the font family and font size presets. Font values are preset slugs, literal font-family values, or null. headingFont falls back to bodyFont when headings do not set their own family. Font size presets list theme sizes first, then any custom sizes; they are not sorted by size.', 'ipsum' ), + 'properties' => array( + 'bodyFont' => array_merge( + $font_value, + array( 'title' => __( 'Body Font', 'ipsum' ) ) + ), + 'headingFont' => array_merge( + $font_value, + array( 'title' => __( 'Heading Font', 'ipsum' ) ) + ), 'blockFonts' => array( 'type' => 'object', - 'description' => __( 'The blocks set in another font than body text, such as buttons, navigation and post meta, grouped by font family slug.', 'ipsum' ), + 'title' => __( 'Block Fonts', 'ipsum' ), + 'description' => __( 'The blocks set in another font than body text, such as buttons, navigation and post meta, grouped by font family slug or literal value.', 'ipsum' ), 'additionalProperties' => $strings, ), - 'fontFamilies' => $preset_list( __( 'The font family presets.', 'ipsum' ), 'fontFamily', $string ), - 'fontSizes' => $preset_list( __( 'The font size presets, smallest first.', 'ipsum' ), 'size', $string ), + 'fontFamilies' => $preset_list( + __( 'Font Families', 'ipsum' ), + __( 'The font family presets from the theme and any added on the site. The name is omitted when a preset does not set one.', 'ipsum' ), + 'fontFamily', + array( + 'type' => 'string', + 'title' => __( 'Font Family', 'ipsum' ), + 'description' => __( 'The CSS font-family value.', 'ipsum' ), + ), + array( 'slug', 'fontFamily' ) + ), + 'fontSizes' => $preset_list( + __( 'Font Sizes', 'ipsum' ), + __( 'The font size presets from the theme, then any custom sizes. Not sorted by size. The name is omitted when a preset does not set one.', 'ipsum' ), + 'size', + array( + 'type' => 'string', + 'title' => __( 'Size', 'ipsum' ), + 'description' => __( 'The CSS font-size value.', 'ipsum' ), + ), + array( 'slug', 'size' ) + ), + ), + 'required' => array( 'bodyFont', 'headingFont', 'blockFonts', 'fontFamilies', 'fontSizes' ), + 'additionalProperties' => false, + ), + 'spacing' => $preset_list( + __( 'Spacing', 'ipsum' ), + __( 'The spacing scale from the theme, then any custom sizes. Not sorted by size. The name is omitted when a preset does not set one.', 'ipsum' ), + 'size', + array( + 'type' => 'string', + 'title' => __( 'Size', 'ipsum' ), + 'description' => __( 'The CSS spacing size value.', 'ipsum' ), ), + array( 'slug', 'size' ) ), - 'spacing' => $preset_list( __( 'The spacing scale, smallest first.', 'ipsum' ), 'size', $string ), 'layout' => array( - 'type' => 'object', - 'description' => __( 'The width of the content, and of wide-aligned blocks.', 'ipsum' ), - 'properties' => array( - 'contentSize' => $nullable, - 'wideSize' => $nullable, + 'type' => 'object', + 'title' => __( 'Layout', 'ipsum' ), + 'description' => __( 'The width of the content, and of wide-aligned blocks.', 'ipsum' ), + 'properties' => array( + 'contentSize' => array( + 'type' => array( 'string', 'null' ), + 'title' => __( 'Content Size', 'ipsum' ), + 'description' => __( 'The default content width, or null when unset.', 'ipsum' ), + ), + 'wideSize' => array( + 'type' => array( 'string', 'null' ), + 'title' => __( 'Wide Size', 'ipsum' ), + 'description' => __( 'The wide-aligned content width, or null when unset.', 'ipsum' ), + ), ), + 'required' => array( 'contentSize', 'wideSize' ), + 'additionalProperties' => false, ), 'styleVariations' => $object_list( - __( 'The style variations. Each one lists only what it changes: colorScheme and colors for the palette, bodyFont and headingFont for the fonts, and blocks for the style properties it sets on each block. Their full data is at /wp/v2/global-styles/themes/{stylesheet}/variations.', 'ipsum' ), + __( 'Style Variations', 'ipsum' ), + __( 'The style variations available for this theme. Each item lists only what that variation changes: colorScheme and colors for the palette, bodyFont and headingFont for the fonts, and blocks for the style properties it sets on each block. Optional fields are omitted when the variation does not change them. Their full data is at /wp/v2/global-styles/themes/{stylesheet}/variations.', 'ipsum' ), array( - 'title' => $string, + 'title' => array( + 'type' => 'string', + 'title' => __( 'Title', 'ipsum' ), + 'description' => __( 'The style variation title.', 'ipsum' ), + ), 'colorScheme' => $scheme, 'colors' => array( 'type' => 'object', + 'title' => __( 'Colors', 'ipsum' ), + 'description' => __( 'Palette colors the variation sets, keyed by slug.', 'ipsum' ), 'additionalProperties' => $string, ), - 'bodyFont' => $nullable, - 'headingFont' => $nullable, + 'bodyFont' => array_merge( + $nullable, + array( 'title' => __( 'Body Font', 'ipsum' ) ) + ), + 'headingFont' => array_merge( + $nullable, + array( 'title' => __( 'Heading Font', 'ipsum' ) ) + ), 'blocks' => array( 'type' => 'object', + 'title' => __( 'Blocks', 'ipsum' ), + 'description' => __( 'Style property names the variation sets on each block, keyed by block name.', 'ipsum' ), 'additionalProperties' => $strings, ), - ) + ), + array( 'title' ) ), 'blockStyles' => $object_list( + __( 'Block Styles', 'ipsum' ), __( 'The block styles registered on this site, beyond the ones blocks define themselves. Add className to a block of one of the blockTypes to apply it.', 'ipsum' ), array( - 'name' => $string, - 'label' => $string, - 'className' => $string, - 'blockTypes' => $strings, - ) + 'name' => array( + 'type' => 'string', + 'title' => __( 'Name', 'ipsum' ), + 'description' => __( 'The block style slug.', 'ipsum' ), + ), + 'label' => array( + 'type' => 'string', + 'title' => __( 'Label', 'ipsum' ), + 'description' => __( 'The human-readable block style label.', 'ipsum' ), + ), + 'className' => array( + 'type' => 'string', + 'title' => __( 'Class Name', 'ipsum' ), + 'description' => __( 'The CSS class to add to the block, such as is-style-outline.', 'ipsum' ), + ), + 'blockTypes' => array_merge( + $strings, + array( + 'title' => __( 'Block Types', 'ipsum' ), + 'description' => __( 'The block names this style can be applied to.', 'ipsum' ), + ) + ), + ), + array( 'name', 'label', 'className', 'blockTypes' ) ), 'patterns' => $object_list( - __( 'The patterns of the theme, without their markup, which is at /wp/v2/block-patterns/patterns. Hidden patterns fill templates and template parts and are left out of the inserter.', 'ipsum' ), + __( 'Patterns', 'ipsum' ), + __( 'The patterns registered by the active theme, without their markup, which is at /wp/v2/block-patterns/patterns. Includes both insertable patterns and hidden ones that fill templates and template parts. Optional fields (blockTypes, templateTypes, postTypes) are omitted when empty.', 'ipsum' ), array( - 'slug' => $string, - 'title' => $string, - 'description' => $string, - 'categories' => $strings, - 'blockTypes' => $strings, - 'templateTypes' => $strings, - 'postTypes' => $strings, - 'hidden' => array( 'type' => 'boolean' ), - ) + 'slug' => array( + 'type' => 'string', + 'title' => __( 'Slug', 'ipsum' ), + 'description' => __( 'The pattern slug.', 'ipsum' ), + ), + 'title' => array( + 'type' => 'string', + 'title' => __( 'Title', 'ipsum' ), + 'description' => __( 'The pattern title.', 'ipsum' ), + ), + 'description' => array( + 'type' => 'string', + 'title' => __( 'Description', 'ipsum' ), + 'description' => __( 'The pattern description.', 'ipsum' ), + ), + 'categories' => array_merge( + $strings, + array( + 'title' => __( 'Categories', 'ipsum' ), + 'description' => __( 'Pattern categories for the inserter.', 'ipsum' ), + ) + ), + 'blockTypes' => array_merge( + $strings, + array( + 'title' => __( 'Block Types', 'ipsum' ), + 'description' => __( 'Block types this pattern is associated with, such as for the block transformer. Omitted when empty.', 'ipsum' ), + ) + ), + 'templateTypes' => array_merge( + $strings, + array( + 'title' => __( 'Template Types', 'ipsum' ), + 'description' => __( 'Template types this pattern is meant to fill. Omitted when empty.', 'ipsum' ), + ) + ), + 'postTypes' => array_merge( + $strings, + array( + 'title' => __( 'Post Types', 'ipsum' ), + 'description' => __( 'Limits which post types the pattern is available for. Omitted when empty.', 'ipsum' ), + ) + ), + 'hidden' => array( + 'type' => 'boolean', + 'title' => __( 'Hidden', 'ipsum' ), + 'description' => __( 'Whether the pattern is excluded from the inserter. Hidden does not mean private; these patterns typically fill templates and template parts.', 'ipsum' ), + ), + ), + array( 'slug', 'title', 'description', 'categories', 'hidden' ) ), 'templates' => $object_list( - __( 'The templates, with the pattern that fills each. The pattern is null when a template holds its own markup, as it does once it is customized.', 'ipsum' ), + __( 'Templates', 'ipsum' ), + __( 'The block templates available on the site, including theme templates and any customized copies, with the pattern each delegates to when its markup is a single Pattern block.', 'ipsum' ), array( - 'slug' => $string, - 'title' => $string, - 'description' => $string, - 'pattern' => $nullable, - 'customized' => array( 'type' => 'boolean' ), - 'postTypes' => $strings, - ) + 'slug' => array( + 'type' => 'string', + 'title' => __( 'Slug', 'ipsum' ), + 'description' => __( 'The template slug.', 'ipsum' ), + ), + 'title' => array( + 'type' => 'string', + 'title' => __( 'Title', 'ipsum' ), + 'description' => __( 'The template title.', 'ipsum' ), + ), + 'description' => array( + 'type' => 'string', + 'title' => __( 'Description', 'ipsum' ), + 'description' => __( 'The template description.', 'ipsum' ), + ), + 'pattern' => array( + 'type' => array( 'string', 'null' ), + 'title' => __( 'Pattern', 'ipsum' ), + 'description' => __( 'The pattern slug when the template markup is a single Pattern block, otherwise null. A customized template can still reference a pattern.', 'ipsum' ), + ), + 'customized' => array( + 'type' => 'boolean', + 'title' => __( 'Customized', 'ipsum' ), + 'description' => __( 'Whether the template has a saved customization on this site.', 'ipsum' ), + ), + 'postTypes' => array_merge( + $strings, + array( + 'title' => __( 'Post Types', 'ipsum' ), + 'description' => __( 'Post types the template applies to. Omitted when empty.', 'ipsum' ), + ) + ), + ), + array( 'slug', 'title', 'description', 'pattern', 'customized' ) ), 'templateParts' => $object_list( - __( 'The template parts, with their area and the pattern that fills each.', 'ipsum' ), + __( 'Template Parts', 'ipsum' ), + __( 'The template parts available on the site, including theme parts and any customized copies, with their area and the pattern each delegates to when its markup is a single Pattern block.', 'ipsum' ), array( - 'slug' => $string, - 'title' => $string, - 'area' => $string, - 'pattern' => $nullable, - 'customized' => array( 'type' => 'boolean' ), - ) + 'slug' => array( + 'type' => 'string', + 'title' => __( 'Slug', 'ipsum' ), + 'description' => __( 'The template part slug.', 'ipsum' ), + ), + 'title' => array( + 'type' => 'string', + 'title' => __( 'Title', 'ipsum' ), + 'description' => __( 'The template part title.', 'ipsum' ), + ), + 'area' => array( + 'type' => array( 'string', 'null' ), + 'title' => __( 'Area', 'ipsum' ), + 'description' => __( 'The template part area, such as header, footer, or uncategorized, or null when the part has no area term.', 'ipsum' ), + ), + 'pattern' => array( + 'type' => array( 'string', 'null' ), + 'title' => __( 'Pattern', 'ipsum' ), + 'description' => __( 'The pattern slug when the template part markup is a single Pattern block, otherwise null. A customized template part can still reference a pattern.', 'ipsum' ), + ), + 'customized' => array( + 'type' => 'boolean', + 'title' => __( 'Customized', 'ipsum' ), + 'description' => __( 'Whether the template part has a saved customization on this site.', 'ipsum' ), + ), + ), + array( 'slug', 'title', 'area', 'pattern', 'customized' ) ), 'templatePartAreas' => $object_list( - __( 'The areas a template part can belong to.', 'ipsum' ), + __( 'Template Part Areas', 'ipsum' ), + __( 'The areas a template part can belong to on this site.', 'ipsum' ), array( - 'area' => $string, - 'label' => $string, - 'description' => $string, - ) + 'area' => array( + 'type' => 'string', + 'title' => __( 'Area', 'ipsum' ), + 'description' => __( 'The template part area slug.', 'ipsum' ), + ), + 'label' => array( + 'type' => 'string', + 'title' => __( 'Label', 'ipsum' ), + 'description' => __( 'The human-readable area label.', 'ipsum' ), + ), + 'description' => array( + 'type' => 'string', + 'title' => __( 'Description', 'ipsum' ), + 'description' => __( 'What the area is for.', 'ipsum' ), + ), + ), + array( 'area', 'label', 'description' ) ), - 'guidelines' => array_merge( - $strings, - array( 'description' => __( 'How the theme is meant to be built with.', 'ipsum' ) ) + 'guidelines' => array( + 'type' => 'array', + 'title' => __( 'Guidelines', 'ipsum' ), + 'description' => __( 'How the theme is meant to be built with.', 'ipsum' ), + 'items' => $string, ), ), ); @@ -348,14 +653,18 @@ static function ( $block ) { * @param array $input { * The ability input. * - * @type string[] $fields Optional. The sections to return. Default all of them. + * @type string[] $fields Optional. The sections to return. When omitted + * or empty, returns the basic design sections + * from ipsum_get_design_guide_default_fields(). * } * @return array The design guide, keyed by section. */ function ipsum_get_design_guide( $input = array() ) { - $sections = array_keys( ipsum_get_design_guide_schema()['properties'] ); + $all_sections = array_keys( ipsum_get_design_guide_schema()['properties'] ); if ( ! empty( $input['fields'] ) ) { - $sections = array_intersect( $sections, $input['fields'] ); + $sections = array_values( array_intersect( $all_sections, $input['fields'] ) ); + } else { + $sections = ipsum_get_design_guide_default_fields(); } $sections = array_flip( $sections ); diff --git a/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php b/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php new file mode 100644 index 0000000000000..c09e72f40febb --- /dev/null +++ b/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php @@ -0,0 +1,427 @@ +user->create( array( 'role' => 'administrator' ) ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + public static function set_up_before_class(): void { + parent::set_up_before_class(); + + // Ipsum isn't the active theme in the test suite, so its functions.php isn't loaded. + $abilities = WP_CONTENT_DIR . '/themes/ipsum/inc/abilities.php'; + if ( ! file_exists( $abilities ) ) { + return; + } + require_once $abilities; + + global $wp_current_filter; + + /* + * The test suite unhooks core's ability categories, and the ability belongs to `site`. + * Pop in `finally` so a setup exception can't leave the simulated action on the stack. + */ + $categories_before = array_keys( wp_get_ability_categories() ); + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + try { + if ( ! wp_has_ability_category( 'site' ) ) { + wp_register_core_ability_categories(); + } + } finally { + array_pop( $wp_current_filter ); + } + self::$registered_categories = array_diff( array_keys( wp_get_ability_categories() ), $categories_before ); + + $wp_current_filter[] = 'wp_abilities_api_init'; + try { + ipsum_register_abilities(); + } finally { + array_pop( $wp_current_filter ); + } + } + + public static function tear_down_after_class(): void { + if ( wp_has_ability( self::ABILITY_NAME ) ) { + wp_unregister_ability( self::ABILITY_NAME ); + } + foreach ( self::$registered_categories as $category ) { + wp_unregister_ability_category( $category ); + } + self::$registered_categories = array(); + + parent::tear_down_after_class(); + } + + public function set_up() { + parent::set_up(); + + $this->original_stylesheet = get_stylesheet(); + + if ( ! function_exists( 'ipsum_register_abilities' ) ) { + $this->markTestSkipped( 'The Ipsum theme is not installed.' ); + } + + switch_theme( 'ipsum' ); + wp_set_current_user( self::$admin_id ); + } + + public function tear_down() { + if ( get_stylesheet() !== $this->original_stylesheet ) { + switch_theme( $this->original_stylesheet ); + } + + parent::tear_down(); + } + + /** + * Tests that the ability is registered as a read-only site ability that the REST API exposes. + * + * @ticket 66678 + */ + public function test_ability_is_registered() { + $ability = wp_get_ability( self::ABILITY_NAME ); + + $this->assertInstanceOf( WP_Ability::class, $ability, 'The ability should be registered.' ); + $this->assertSame( 'site', $ability->get_category(), 'The ability should be in the site category.' ); + $this->assertSame( array( 'edit_theme_options' ), $ability->get_required_capabilities(), 'The ability should need edit_theme_options.' ); + $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ), 'The ability should be shown in the REST API.' ); + + $annotations = $ability->get_meta_item( 'annotations', array() ); + $this->assertTrue( $annotations['readonly'], 'The ability should be annotated as read-only.' ); + $this->assertFalse( $annotations['destructive'], 'The ability should be annotated as not destructive.' ); + $this->assertTrue( $annotations['idempotent'], 'The ability should be annotated as idempotent.' ); + } + + /** + * Tests that the input schema offers every output section, and that every section is described. + * + * @ticket 66678 + */ + public function test_schemas_list_and_describe_every_section() { + $ability = wp_get_ability( self::ABILITY_NAME ); + $input_schema = $ability->get_input_schema(); + $output_schema = $ability->get_output_schema(); + + $this->assertSame( array(), $input_schema['default'], 'The input should default to an empty object.' ); + $this->assertFalse( $input_schema['additionalProperties'], 'The input should only accept fields.' ); + $this->assertSame( array_keys( $output_schema['properties'] ), $input_schema['properties']['fields']['items']['enum'], 'Fields should offer every output section.' ); + + foreach ( $output_schema['properties'] as $section => $section_schema ) { + $this->assertNotEmpty( $section_schema['title'] ?? '', "The $section section should have a title." ); + $this->assertNotEmpty( $section_schema['description'] ?? '', "The $section section should have a description." ); + } + } + + /** + * Tests that the basic sections are returned when no fields, or no fields at all, are asked for. + * + * @ticket 66678 + * + * @dataProvider data_inputs_without_fields + * + * @param array|null $input The ability input. + */ + public function test_returns_the_basic_sections_by_default( $input ) { + $result = wp_get_ability( self::ABILITY_NAME )->execute( $input ); + + $this->assertIsArray( $result, 'The ability should return a guide that matches its output schema.' ); + $this->assertSameSets( ipsum_get_design_guide_default_fields(), array_keys( $result ), 'The guide should hold the basic sections and nothing else.' ); + $this->assertArrayNotHasKey( 'patterns', $result, 'The guide should leave out the larger lists by default.' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_inputs_without_fields() { + return array( + 'no input' => array( null ), + 'empty input' => array( array() ), + 'empty fields' => array( array( 'fields' => array() ) ), + ); + } + + /** + * Tests that each section can be asked for on its own, and matches the output schema. + * + * @ticket 66678 + */ + public function test_returns_each_section_on_its_own() { + $ability = wp_get_ability( self::ABILITY_NAME ); + $sections = $ability->get_input_schema()['properties']['fields']['items']['enum']; + + foreach ( $sections as $section ) { + $result = $ability->execute( array( 'fields' => array( $section ) ) ); + + $this->assertIsArray( $result, "The $section section should match the output schema." ); + $this->assertSame( array( $section ), array_keys( $result ), "Asking for $section should return only that section." ); + } + } + + /** + * Tests that asking for several sections returns just those. + * + * @ticket 66678 + */ + public function test_returns_only_the_requested_sections() { + $result = wp_get_ability( self::ABILITY_NAME )->execute( array( 'fields' => array( 'colors', 'patterns' ) ) ); + + $this->assertIsArray( $result, 'The ability should return a guide that matches its output schema.' ); + $this->assertSameSets( array( 'colors', 'patterns' ), array_keys( $result ), 'The guide should hold only the requested sections.' ); + } + + /** + * Tests that input the schema doesn't allow is rejected. + * + * @ticket 66678 + * + * @dataProvider data_invalid_inputs + * + * @param mixed $input The ability input. + */ + public function test_rejects_invalid_input( $input ) { + $result = wp_get_ability( self::ABILITY_NAME )->execute( $input ); + + $this->assertWPError( $result, 'Invalid input should be rejected.' ); + $this->assertSame( 'ability_invalid_input', $result->get_error_code(), 'The error should be about the input.' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_invalid_inputs() { + return array( + 'unknown section' => array( array( 'fields' => array( 'not-a-section' ) ) ), + 'unknown property' => array( array( 'sections' => array( 'colors' ) ) ), + 'unknown section in a list' => array( array( 'fields' => 'colors,not-a-section' ) ), + ); + } + + /** + * Tests that people who can't edit theme options can't get the guide. + * + * @ticket 66678 + * + * @dataProvider data_roles_without_edit_theme_options + * + * @param string $role The user's role, or an empty string for a logged-out visitor. + */ + public function test_denies_users_who_cannot_edit_theme_options( $role ) { + wp_set_current_user( '' === $role ? 0 : self::factory()->user->create( array( 'role' => $role ) ) ); + + $ability = wp_get_ability( self::ABILITY_NAME ); + $result = $ability->execute(); + + $this->assertFalse( $ability->check_permissions(), 'The permission check should fail.' ); + $this->assertWPError( $result, 'The ability should not run.' ); + $this->assertSame( 'ability_invalid_permissions', $result->get_error_code(), 'The error should be about permissions.' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_roles_without_edit_theme_options() { + return array( + 'logged out' => array( '' ), + 'subscriber' => array( 'subscriber' ), + 'contributor' => array( 'contributor' ), + 'author' => array( 'author' ), + 'editor' => array( 'editor' ), + ); + } + + /** + * Tests that a user given edit_theme_options can get the guide, whatever their role. + * + * @ticket 66678 + */ + public function test_allows_users_given_edit_theme_options() { + $user = wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) ); + $user->add_cap( 'edit_theme_options' ); + + $ability = wp_get_ability( self::ABILITY_NAME ); + + $this->assertTrue( $ability->check_permissions(), 'The permission check should pass.' ); + $this->assertIsArray( $ability->execute(), 'The ability should run.' ); + } + + /** + * Tests running the ability through the REST API, with and without fields. + * + * @ticket 66678 + */ + public function test_runs_through_the_rest_api() { + $server = $this->set_up_rest_server(); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/' . self::ABILITY_NAME . '/run' ); + $response = $server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'A read-only ability should run with GET.' ); + $this->assertSameSets( ipsum_get_design_guide_default_fields(), array_keys( $response->get_data() ), 'The response should hold the basic sections.' ); + + $request->set_query_params( array( 'input' => array( 'fields' => array( 'templates' ) ) ) ); + $response = $server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'The ability should run with fields.' ); + $this->assertSame( array( 'templates' ), array_keys( $response->get_data() ), 'The response should hold only the requested section.' ); + } + + /** + * Tests that the REST API doesn't run the ability for a logged-out visitor. + * + * @ticket 66678 + */ + public function test_rest_api_denies_logged_out_visitors() { + $server = $this->set_up_rest_server(); + wp_set_current_user( 0 ); + + $response = $server->dispatch( new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/' . self::ABILITY_NAME . '/run' ) ); + + $this->assertSame( 401, $response->get_status(), 'A logged-out visitor should not run the ability.' ); + } + + /** + * Tests that the guide follows the styles saved on the site, such as a chosen color variation. + * + * @ticket 66678 + */ + public function test_follows_the_saved_global_styles() { + $before = wp_get_ability( self::ABILITY_NAME )->execute( array( 'fields' => array( 'colorScheme', 'colors' ) ) ); + $this->assertSame( 'light', $before['colorScheme'], 'The default palette should be light.' ); + + // Save the Evening variation as the site's styles, as choosing it in the Site Editor does. + $variation = wp_json_file_decode( get_theme_file_path( 'styles/colors/06-evening.json' ), array( 'associative' => true ) ); + $this->assertSame( 'dark', $variation['settings']['custom']['controlScheme'], 'Evening should be a dark variation.' ); + + $user_styles = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'settings' => $variation['settings'], + ); + // Saved styles key the palette by origin. + $user_styles['settings']['color']['palette'] = array( 'theme' => $variation['settings']['color']['palette'] ); + unset( $user_styles['settings']['color']['duotone'] ); + + self::factory()->post->create( + array( + 'post_content' => wp_json_encode( $user_styles ), + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-ipsum', + 'tax_input' => array( 'wp_theme' => 'ipsum' ), + ) + ); + WP_Theme_JSON_Resolver::clean_cached_data(); + wp_clean_theme_json_cache(); + + $after = wp_get_ability( self::ABILITY_NAME )->execute( array( 'fields' => array( 'colorScheme', 'colors' ) ) ); + + $this->assertSame( 'dark', $after['colorScheme'], 'The guide should follow the saved variation.' ); + $this->assertSame( + wp_list_pluck( $variation['settings']['color']['palette'], 'color', 'slug' ), + wp_list_pluck( $after['colors'], 'color', 'slug' ), + 'The colors should be the saved variation\'s.' + ); + } + + /** + * Tests that the guide marks a template saved on the site as customized. + * + * @ticket 66678 + */ + public function test_marks_customized_templates() { + $before = $this->get_template( 'index' ); + $this->assertFalse( $before['customized'], 'The theme\'s index template should not be customized.' ); + $this->assertSame( 'ipsum/hidden-index', $before['pattern'], 'The theme\'s index template should hold its pattern.' ); + + $template_id = self::factory()->post->create( + array( + 'post_type' => 'wp_template', + 'post_name' => 'index', + 'post_title' => 'Index', + 'post_content' => '

A customized index.

', + 'tax_input' => array( 'wp_theme' => array( 'ipsum' ) ), + ) + ); + wp_set_post_terms( $template_id, 'ipsum', 'wp_theme' ); + + $after = $this->get_template( 'index' ); + $this->assertTrue( $after['customized'], 'The saved index template should be customized.' ); + $this->assertNull( $after['pattern'], 'The saved index template no longer holds a single pattern.' ); + } + + /** + * Sets up a REST server with the routes registered. + * + * @return WP_REST_Server The server. + */ + private function set_up_rest_server() { + global $wp_rest_server; + + $wp_rest_server = new WP_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + + return $wp_rest_server; + } + + /** + * Returns one template from the templates section of the guide. + * + * @param string $slug The template slug. + * @return array The template. + */ + private function get_template( $slug ) { + $guide = wp_get_ability( self::ABILITY_NAME )->execute( array( 'fields' => array( 'templates' ) ) ); + $templates = array_column( $guide['templates'], null, 'slug' ); + + $this->assertArrayHasKey( $slug, $templates, "The guide should list the $slug template." ); + + return $templates[ $slug ]; + } +} From 3e7e8735cb963fd85ba0313ddb9047a29e26ce15 Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:39 +0100 Subject: [PATCH 05/13] Ipsum: Make the short post date format translatable. The Archive Minimal, Archive Simple and All Archives Sidebar patterns hard-coded the `M j` date format, so the short date couldn't follow the site's language. The format is now a translatable string. Synced from https://github.com/WordPress/ipsum/pull/224. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/patterns/archive-minimal.php | 2 +- src/wp-content/themes/ipsum/patterns/archive-simple.php | 2 +- src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-content/themes/ipsum/patterns/archive-minimal.php b/src/wp-content/themes/ipsum/patterns/archive-minimal.php index 4a0f40575f2b8..370deec6f7822 100644 --- a/src/wp-content/themes/ipsum/patterns/archive-minimal.php +++ b/src/wp-content/themes/ipsum/patterns/archive-minimal.php @@ -17,7 +17,7 @@
-
+
diff --git a/src/wp-content/themes/ipsum/patterns/archive-simple.php b/src/wp-content/themes/ipsum/patterns/archive-simple.php index 35590e17463f4..18f3d38a8545c 100644 --- a/src/wp-content/themes/ipsum/patterns/archive-simple.php +++ b/src/wp-content/themes/ipsum/patterns/archive-simple.php @@ -18,7 +18,7 @@
-
+

diff --git a/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php b/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php index 7700746c58f9a..ab48582017a4b 100644 --- a/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php +++ b/src/wp-content/themes/ipsum/patterns/hidden-archive-sidebar.php @@ -26,7 +26,7 @@
-
+

From 34c3fe4b9009c2e5f99e894d283240aaa3622352 Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:39 +0100 Subject: [PATCH 06/13] Ipsum: Fix Details block markers being announced by screen readers. The Details block's triangle markers get empty text alternatives, so screen readers don't read them aloud. They stay visible, and the block still announces whether it's expanded. Synced from https://github.com/WordPress/ipsum/pull/230. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-content/themes/ipsum/theme.json b/src/wp-content/themes/ipsum/theme.json index 05ecc05dca107..1cdf49abc5ca3 100644 --- a/src/wp-content/themes/ipsum/theme.json +++ b/src/wp-content/themes/ipsum/theme.json @@ -745,7 +745,7 @@ "width": "1px" } }, - "css": "& summary{font-family:var(--wp--preset--font-family--manrope);font-size:var(--wp--preset--font-size--small);font-style:normal;font-weight:700;list-style:none;display:flex;align-items:baseline;gap:var(--wp--preset--spacing--20);} & summary::-webkit-details-marker{display:none;} & summary::before{content:\"\\25B8\";} &[open] > summary::before{content:\"\\25BE\";}", + "css": "& summary{font-family:var(--wp--preset--font-family--manrope);font-size:var(--wp--preset--font-size--small);font-style:normal;font-weight:700;list-style:none;display:flex;align-items:baseline;gap:var(--wp--preset--spacing--20);} & summary::-webkit-details-marker{display:none;} & summary::before{content:\"\\25B8\" / \"\";} &[open] > summary::before{content:\"\\25BE\" / \"\";}", "spacing": { "padding": { "bottom": "var(--wp--preset--spacing--20)", From cec447b244aa78977778e874700f9f744c341b8a Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:22:40 +0100 Subject: [PATCH 07/13] Ipsum: Remove the physical left alignment from the page patterns. The Bio, Blog Home, Coming Soon and Text Statement Wide patterns set a left text alignment on seven headings and paragraphs, so they stayed on the left on RTL sites. Without it, they follow the text direction. Synced from https://github.com/WordPress/ipsum/pull/226. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/patterns/page-bio.php | 4 ++-- .../themes/ipsum/patterns/page-blog-home.php | 12 ++++++------ src/wp-content/themes/ipsum/patterns/page-soon.php | 8 ++++---- .../themes/ipsum/patterns/text-statement-wide.php | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/wp-content/themes/ipsum/patterns/page-bio.php b/src/wp-content/themes/ipsum/patterns/page-bio.php index daf56db88c171..c42fb860bac42 100644 --- a/src/wp-content/themes/ipsum/patterns/page-bio.php +++ b/src/wp-content/themes/ipsum/patterns/page-bio.php @@ -25,8 +25,8 @@
-
-

+
+

diff --git a/src/wp-content/themes/ipsum/patterns/page-blog-home.php b/src/wp-content/themes/ipsum/patterns/page-blog-home.php index 9b6b85ad9b8f7..f832e81553eb5 100644 --- a/src/wp-content/themes/ipsum/patterns/page-blog-home.php +++ b/src/wp-content/themes/ipsum/patterns/page-blog-home.php @@ -15,8 +15,8 @@ ?>
-
-

essays.', 'ipsum' ) ); ?>

+
+

essays.', 'ipsum' ) ); ?>

@@ -88,12 +88,12 @@
diff --git a/src/wp-content/themes/ipsum/patterns/header-vertical.php b/src/wp-content/themes/ipsum/patterns/header-vertical.php index e5d515dec23f2..211ae216707fa 100644 --- a/src/wp-content/themes/ipsum/patterns/header-vertical.php +++ b/src/wp-content/themes/ipsum/patterns/header-vertical.php @@ -15,7 +15,7 @@
-
+
diff --git a/src/wp-content/themes/ipsum/patterns/hero-colophon.php b/src/wp-content/themes/ipsum/patterns/hero-colophon.php index b11e4fb22048d..8a57c7778f12a 100644 --- a/src/wp-content/themes/ipsum/patterns/hero-colophon.php +++ b/src/wp-content/themes/ipsum/patterns/hero-colophon.php @@ -26,7 +26,7 @@

-
+
diff --git a/src/wp-content/themes/ipsum/patterns/hero-epigraph.php b/src/wp-content/themes/ipsum/patterns/hero-epigraph.php index 1392ac9bce746..75be2bd11bba9 100644 --- a/src/wp-content/themes/ipsum/patterns/hero-epigraph.php +++ b/src/wp-content/themes/ipsum/patterns/hero-epigraph.php @@ -23,7 +23,7 @@
-
+
diff --git a/src/wp-content/themes/ipsum/patterns/navigation-overlay.php b/src/wp-content/themes/ipsum/patterns/navigation-overlay.php index 2a1f9eb28a828..7112d8511d4fc 100644 --- a/src/wp-content/themes/ipsum/patterns/navigation-overlay.php +++ b/src/wp-content/themes/ipsum/patterns/navigation-overlay.php @@ -31,7 +31,7 @@ -
+
diff --git a/src/wp-content/themes/ipsum/patterns/overlay-contents.php b/src/wp-content/themes/ipsum/patterns/overlay-contents.php index 23e00ec4f1b06..6a9b0c2bb1b73 100644 --- a/src/wp-content/themes/ipsum/patterns/overlay-contents.php +++ b/src/wp-content/themes/ipsum/patterns/overlay-contents.php @@ -23,7 +23,7 @@ -
+
diff --git a/src/wp-content/themes/ipsum/patterns/overlay-profile.php b/src/wp-content/themes/ipsum/patterns/overlay-profile.php index 2862042f230da..5133f3d566ad0 100644 --- a/src/wp-content/themes/ipsum/patterns/overlay-profile.php +++ b/src/wp-content/themes/ipsum/patterns/overlay-profile.php @@ -35,7 +35,7 @@ -
+
From 624f2e18943e7f20e426a2cf14bbc3a53c17274c Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:23:05 +0100 Subject: [PATCH 10/13] Bundled Themes: Update Ipsum's core-only files from the 7.2 Beta 5 review. `contributing.txt` says Ipsum is developed on GitHub until 1.0, and that contributions go through Trac after that. It also drops the note about `style.css` being used when it's newer than `style.min.css`, since the theme now checks only `SCRIPT_DEBUG`. A test message in the design guide tests says "do not match" instead of "to not match". Follow-up to [65366]. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/contributing.txt | 4 ++-- tests/phpunit/tests/theme/ipsumDesignGuide.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-content/themes/ipsum/contributing.txt b/src/wp-content/themes/ipsum/contributing.txt index 2b4f5cce576a2..fb8cf7fdf51ab 100644 --- a/src/wp-content/themes/ipsum/contributing.txt +++ b/src/wp-content/themes/ipsum/contributing.txt @@ -1,10 +1,10 @@ === Contributing to Ipsum === -Ipsum is developed at https://github.com/WordPress/ipsum. Changes made there are synced to WordPress core, so please open issues and pull requests in that repository. +Ipsum is developed on GitHub, at https://github.com/WordPress/ipsum, until its 1.0 release. Changes made there are synced to WordPress core, so until then, please open issues and pull requests in that repository. After 1.0, the GitHub repository is archived, and contributions go through Trac, https://core.trac.wordpress.org, like for the other bundled themes. = Minifying CSS = -Ipsum has a single stylesheet `style.css` which is enqueued in addition to the global styles coming from core. On a normal production site, when `SCRIPT_DEBUG` is disabled, then the minified version `style.min.css` will be enqueued instead, unless `style.css` has changed since `style.min.css` was built. If you make a change to `style.css`, you'll need to re-minify the `style.min.css` using the built-in npm build tool. As always, it is preferable to use the Site Editor to supply Additional CSS instead of directly editing the theme stylesheet. +Ipsum has a single stylesheet `style.css` which is enqueued in addition to the global styles coming from core. On a normal production site, when `SCRIPT_DEBUG` is disabled, then the minified version `style.min.css` will be enqueued instead. If you make a change to `style.css`, you'll need to re-minify the `style.min.css` using the built-in npm build tool. As always, it is preferable to use the Site Editor to supply Additional CSS instead of directly editing the theme stylesheet. Installation instructions diff --git a/tests/phpunit/tests/theme/ipsumDesignGuide.php b/tests/phpunit/tests/theme/ipsumDesignGuide.php index dd43ff57e75ef..c2f69f9bb5fd8 100644 --- a/tests/phpunit/tests/theme/ipsumDesignGuide.php +++ b/tests/phpunit/tests/theme/ipsumDesignGuide.php @@ -134,7 +134,7 @@ public function test_returns_child_and_parent_theme_patterns() { $this->assertContains( 'block-theme-with-hooked-blocks-child/child-pattern', $expected, 'The child theme should have a pattern of its own.' ); $this->assertContains( 'block-theme-with-hooked-blocks/footer-default', $expected, 'The parent theme should have a pattern.' ); - $this->assertSame( $expected, $this->get_design_guide_pattern_slugs(), 'The theme pattern slugs to not match.' ); + $this->assertSame( $expected, $this->get_design_guide_pattern_slugs(), 'The theme pattern slugs do not match.' ); } /** From b982de397c49cac8614771edb860b0f07a4c76bb Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:31:23 +0100 Subject: [PATCH 11/13] Ipsum: Split AGENTS.md so it holds for any copy of the theme. `AGENTS.md` now covers the theme itself, so it ships with the theme, like `DESIGN.md`, and helps agents that edit Ipsum or build on it anywhere. What only applies to the GitHub repository stays there, in `CONTRIBUTING.md`, and `DESIGN.md` links to `AGENTS.md` for working on the theme's files. Synced from https://github.com/WordPress/ipsum/pull/214. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/AGENTS.md | 57 ++++++++++++++++++++++++ src/wp-content/themes/ipsum/DESIGN.md | 64 ++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/wp-content/themes/ipsum/AGENTS.md diff --git a/src/wp-content/themes/ipsum/AGENTS.md b/src/wp-content/themes/ipsum/AGENTS.md new file mode 100644 index 0000000000000..3ccc1ffc55c6e --- /dev/null +++ b/src/wp-content/themes/ipsum/AGENTS.md @@ -0,0 +1,57 @@ +# AGENTS.md + +How coding agents should work on Ipsum, in its GitHub repository or in any other copy of the theme, such as the one bundled with WordPress. This file covers what is specific to the theme and easy to get wrong. [DESIGN.md](DESIGN.md) says how to design with it. + +## Working in the GitHub repository + +In the [GitHub repository](https://github.com/WordPress/ipsum), read [CONTRIBUTING.md](https://github.com/WordPress/ipsum/blob/trunk/CONTRIBUTING.md) before you set up the development environment, run the checks or open a pull request. Other copies of the theme, such as the one in WordPress core, don't have that tooling. + +## Key directories + +Templates, patterns, `theme.json` and CSS are served as they are in the tree. The only generated file is `style.min.css`. + +- `templates/*.html` - Block templates. Each is a one-line delegate to a pattern. +- `parts/*.html` - Template parts. Each is a one-line delegate to a pattern. +- `patterns/*.php` - The markup of every template and part, plus the insertable patterns. +- `styles/colors/*.json` - Color style variations, number-prefixed to control their order. +- `styles/typography/*.json` - Typography style variations. +- `styles/blocks/*.json` - Block style variations. The `slug` is used in markup as `is-style-`. +- `assets/fonts/` - Self-hosted fonts, one directory per family, credited under `== Fonts ==` in `readme.txt`. +- `assets/css/editor-style.css` - Editor-only overrides, loaded after `style.css` in the editors. +- `functions.php` - Theme PHP: stylesheets, the comments CTA block binding, sidebar template types, block styles. It loads `inc/abilities.php`. +- `inc/abilities.php` - The `ipsum/get-design-guide` ability, which gives agents on a live site the palette roles, presets, style variations, block styles, patterns and templates through the Abilities API. A new palette color needs its role in `ipsum_get_color_roles()`. +- `theme.json` - Global settings and styles, fonts, `customTemplates`, per-block styles. +- `style.css` - The theme header plus the CSS that `theme.json` cannot express. It loads in the editors through `add_editor_style()`, and is inlined on the front end when `SCRIPT_DEBUG` is on. +- `style.min.css` - `style.css` minified by `npm run build`, inlined on the front end otherwise. Generated: rebuild it, never edit it. +- `readme.txt` - The WordPress.org readme: description, changelog, copyright, font credits. + +## Progressive discovery + +Read only what your task needs, when it needs it: + +- **Styling, or building pages, patterns or templates**: read [DESIGN.md](DESIGN.md) first. It covers the color roles, the type and spacing scales, the style variations, and which pattern fills each template and part. +- **Adding or changing a pattern**: read [Writing patterns](DESIGN.md#writing-patterns) in DESIGN.md first. It covers categories, hiding from the inserter, which i18n function to use, images, and removing `id`, `queryId`, `theme`, `area`, `patternName` and `metadata.categories` from copied markup. +- **Adding CSS or PHP**: read [Development guidelines](https://github.com/WordPress/ipsum/blob/trunk/CONTRIBUTING.md#development-guidelines). Prefer `theme.json` and Global Styles; add CSS or PHP only when the editor cannot express the design, and keep it commented. + +## Markdown files + +Ipsum's Markdown files are soft-wrapped: a paragraph, list item or table row is one line, however long. Do not hard-wrap prose, and do not re-flow lines a change does not otherwise touch. + +## Architectural decisions + +- **The pattern is the source of truth.** `templates/index.html` is `` and nothing else. Editing a delegate template or part changes nothing at render time; edit the pattern it names. A pattern that only fills a template or part is hidden from the inserter with `Inserter: no`, and its file and slug take the `hidden-` prefix (`patterns/hidden-index.php` is `ipsum/hidden-index`); template patterns also carry `Template Types:`. Insertable patterns use `Categories`, `Block Types`, `Viewport width` and `Description`. +- **Style with the design tokens, not literals.** Colors come from `var(--wp--preset--color--)`, spacing from `var:preset|spacing|N`. The palette uses the Twenty Twenty-Five slugs, so default theme patterns pick up Ipsum's colors: `base` (Base) is the background, `contrast` (Contrast) the text, headings, links and buttons, `accent-1` (Accent) the accent, which only the arrival bar uses, `accent-3` (Strong Contrast) the hover color and the ground of inverted sections and cards, `accent-5` (Surface) the faint wash behind quotes, tags and cards, and `accent-6` (Border) the faint lines. `accent-2` and `accent-4` are not defined. Every color variation redefines all six, so a literal hex freezes one variation's look into all the others. `settings.custom.controlScheme` says whether the active palette is `light` or `dark`. +- **CSS explains why, in prose.** Rules in `style.css` and `assets/css/editor-style.css` carry a comment saying why they exist, often with the Gutenberg or Trac issue behind it. `css` strings in `theme.json` cannot hold comments. +- **Text domain is `ipsum`.** Every user-visible string goes through an i18n function, including single characters such as the `·` separators. +- **PHP follows the WordPress coding standards**, in tabs. Functions in `functions.php` and `inc/` are wrapped in `if ( ! function_exists( '…' ) ) : … endif;`, every docblock carries `@since Ipsum 1.0`, and `@package Ipsum` heads the file. + +## Do not reformat block markup + +The delimiters in templates, parts and patterns are Gutenberg's serialization, not hand-written HTML: flush left, one delimiter per line, a blank line between sibling blocks, and a block's wrapper element on the same line as the opening delimiter of its first child: + +```html + +
+``` + +Reindenting or rewrapping it produces a large diff that the next editor round-trip undoes. diff --git a/src/wp-content/themes/ipsum/DESIGN.md b/src/wp-content/themes/ipsum/DESIGN.md index efc7d52aa4a02..fc6c76088e9e0 100644 --- a/src/wp-content/themes/ipsum/DESIGN.md +++ b/src/wp-content/themes/ipsum/DESIGN.md @@ -1,6 +1,6 @@ # DESIGN.md -How to design with Ipsum: what each preset is for, how the theme uses it, and what the style variations, templates, parts and block styles give you. `AGENTS.md` covers working in the repository. +How to design with Ipsum: what each preset is for, how the theme uses it, and what the style variations, templates, parts and block styles give you. [Writing patterns](#writing-patterns) covers what a new pattern needs. [AGENTS.md](AGENTS.md) covers working on the theme's files. ## Ipsum @@ -251,3 +251,65 @@ The separator between meta items is built three ways, on purpose: - **Where both neighbours always render** (post date · categories), it's a Paragraph named Meta Separator: small, weight 600, with the dot hidden from screen readers. The row's block gap (`spacing|20`) spaces it. To add one, copy it from an archive pattern. - **Where the whole row can go** (comments link · call to action), it's the same Paragraph, and the `comments-meta` class hides the row when comments are closed. - **Where a neighbour may not render**, it's the Dot Separator style. The Edit link only renders for people who can edit the comment, and the Reply link only when threading allows it, so a Paragraph would be left behind as a stray dot. Use the Paragraph wherever it's safe: unlike the style's dot, it can be translated. + +## Writing patterns + +What a pattern in Ipsum needs, on top of the [block patterns reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-patterns/). + +### Files and headers + +- **File names:** full page layouts take the `page-` prefix, and patterns that only fill a template or part take `hidden-`. The inserter sorts patterns by file name, so the name also sets their order. +- **Categories:** use the [default categories](https://github.com/WordPress/gutenberg/blob/c20350c1d246163201375f090b0b7b4ab49b1dad/packages/block-editor/src/components/inserter/block-patterns-tab.js#L35), separated by commas, plus the two that `functions.php` registers: `ipsum_page` (Pages) for full page layouts and `ipsum_post-format` (Post formats) for post format patterns. +- **Hiding from the inserter:** a pattern people shouldn't insert on its own, such as one that only fills a template or part, or one that exists to make a template's text translatable, like the 404 pattern, carries ` * Inserter: no`. +- **Title and Description:** every pattern needs both, hidden ones too, since agents read them through the REST API (`/wp/v2/block-patterns/patterns`). Write full sentences, not dashes: what the pattern is, then a second sentence on what's in it. A hidden pattern says which template or template part it fills. +- **Block Types:** suggests the pattern when someone inserts that block or template part, such as a Query, Post Content, header or footer. Header, footer and navigation overlay patterns take `Block Types: core/template-part/`. Patterns for General parts, like the sidebar and the search bar, leave it out, or the Site Editor offers each one as a design for every other General part. +- **Template Types:** suggests the pattern for a template, by slug (`404`, `home`, `single` and so on). Every template pattern takes it. +- **Post Types:** limits the pattern to those post types, usually for full page patterns. + +### Strings + +Every user-visible string goes through an [i18n function](https://developer.wordpress.org/apis/internationalization/internationalization-guidelines/) with the `ipsum` text domain, so it's translatable and escaped: + +- `esc_html__()` translates and escapes text shown in HTML, and `esc_html_x()` does the same with a context for translators. `esc_html_e()` echoes it as well. +- `esc_attr__()` and `esc_attr_x()` translate and escape text that goes in an HTML attribute, such as alt text. +- For simple HTML inside a string, use `echo wp_kses_post( __( 'Lorem ipsum Hello dolor sit amet.', 'ipsum' ) );`. It's clearer for translators than `sprintf()`, and they can drop the markup where it doesn't work in their language. + +### Images + +- Build image URLs with `get_parent_theme_file_uri()` and the image's path in the theme. The URL then works wherever the site lives and from a child theme, and it can be filtered with `parent_theme_file_uri`, for example to serve the images from a CDN. +- Don't add a `?ver=` query argument. The images won't change after release, and once a pattern is inserted, its image URL is saved in the post, so the version would never update there anyway. +- Give every image translated alt text, and remove its ID, both the `id` attribute and the `wp-image-` class. +- Credit every image's license under `== Images ==` in `readme.txt`. + +An image copied from the editor: + +```html + +
+ +``` + +becomes: + +```html + +
<?php echo esc_attr_x( 'Picture of a building', 'Alt text for project picture', 'ipsum' ); ?>
+ +``` + +### Presets + +- Use the spacing, font size and color presets, never fixed values. They keep the design consistent, let a style variation or Global Styles change it in one place, and keep patterns readable for more people. The sections above say what each preset is for. +- If you keep setting the same property on the same block, such as a border radius on images, move it to `theme.json`. + +### Cleaning up copied markup + +Markup built in the editor, or exported with a tool like Create Block Theme, carries attributes that don't belong in a pattern. Remove them; the editor adds back what it needs when the pattern is inserted. + +- **`queryId` on Query blocks**, like the image IDs above: `` becomes ``. +- **`theme` and `area` on template part blocks.** The area comes from `templateParts` in `theme.json`, so the slug is enough: `` becomes ``. +- **`patternName` and `categories` in block metadata.** The editor writes them into blocks inserted from a pattern. Keep the rest of the metadata, such as `name` or `bindings`: `` becomes ``. + +### Block names + +Block names (`metadata.name`) show in the List View, and they're what an agent reads to tell blocks apart. Name a block by what it does, not what it prints: the paragraph holding the `·` separator is `Meta Separator`, not `·`. Translate names like other strings: `esc_html_x( 'Meta Separator', 'Name of the paragraph separating post meta items', 'ipsum' )`. From 2a1a953e7799ee3b8a83784d1cd8e0ddd305202c Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:31:23 +0100 Subject: [PATCH 12/13] Ipsum: Use a logical margin for the comment form checkbox. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The space between the comment form's "Save my name…" checkbox and its label was always on the right, so it shrank on RTL sites. The checkbox now uses logical margins, which follow the text direction. LTR sites look the same as before. Synced from https://github.com/WordPress/ipsum/pull/231. See #66678. Co-Authored-By: Claude Opus 5.5 --- src/wp-content/themes/ipsum/theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-content/themes/ipsum/theme.json b/src/wp-content/themes/ipsum/theme.json index 34c7e045d5551..6d8addf929cf8 100644 --- a/src/wp-content/themes/ipsum/theme.json +++ b/src/wp-content/themes/ipsum/theme.json @@ -842,7 +842,7 @@ } }, "core/post-comments-form": { - "css": "& textarea{border-color: var(--wp--preset--color--contrast);} & input:not([type=submit]){border-color: var(--wp--preset--color--contrast);} & textarea:hover{border-color: var(--wp--preset--color--accent-3);} & input:not([type=submit]):hover{border-color: var(--wp--preset--color--accent-3);} & input[type=checkbox]{margin:0 .5rem 0 0 !important;} & .comment-form > * {margin-top: var(--wp--style--block-gap);margin-bottom: 0;}", + "css": "& textarea{border-color: var(--wp--preset--color--contrast);} & input:not([type=submit]){border-color: var(--wp--preset--color--contrast);} & textarea:hover{border-color: var(--wp--preset--color--accent-3);} & input:not([type=submit]):hover{border-color: var(--wp--preset--color--accent-3);} & input[type=checkbox]{margin-block:0 !important;margin-inline-start:0;margin-inline-end:.5rem;} & .comment-form > * {margin-top: var(--wp--style--block-gap);margin-bottom: 0;}", "elements": { "label": { "typography": { From ec4da5b1a163f967a4d1c43e6c758ae3f4ca1b49 Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 12 Nov 2026 16:44:30 +0100 Subject: [PATCH 13/13] Bundled Themes: Fix coding standards in the Ipsum design guide ability tests. Aligns the array arrows in a data provider, which the PHP coding standards check flagged. See #66678. Co-Authored-By: Claude Opus 5.5 --- tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php b/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php index c09e72f40febb..1b2ecdaf97b0c 100644 --- a/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php +++ b/tests/phpunit/tests/theme/ipsumGetDesignGuideAbility.php @@ -234,8 +234,8 @@ public function test_rejects_invalid_input( $input ) { */ public static function data_invalid_inputs() { return array( - 'unknown section' => array( array( 'fields' => array( 'not-a-section' ) ) ), - 'unknown property' => array( array( 'sections' => array( 'colors' ) ) ), + 'unknown section' => array( array( 'fields' => array( 'not-a-section' ) ) ), + 'unknown property' => array( array( 'sections' => array( 'colors' ) ) ), 'unknown section in a list' => array( array( 'fields' => 'colors,not-a-section' ) ), ); }