From 62f0c5ef75d08a95a878a03ac6fb179ddcb1b477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 12 Dec 2025 14:12:15 +0100 Subject: [PATCH 01/22] Admin UI: Add design system tokens for admin reskin Introduces _tokens.scss with Sass variables derived from the WordPress Design System in Figma. These tokens provide consistent values for: - Spacing (4px grid units) - Border radius - Gray scale - Semantic colors (alerts/notices) - Typography scale - Elevation (box shadows) - Component sizing (buttons, inputs, checkboxes) The tokens are imported into _variables.scss and can be used across all admin stylesheets compiled via Sass. Note: These are Sass-only variables. No new CSS custom properties are exposed to maintain backward compatibility. The only CSS custom properties available remain those from wp-base-styles. Part of the WordPress 7.0 admin visual reskin initiative. See: https://core.trac.wordpress.org/ticket/64308 --- src/wp-admin/css/colors/_tokens.scss | 212 ++++++++++++++++++++++++ src/wp-admin/css/colors/_variables.scss | 5 +- 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/wp-admin/css/colors/_tokens.scss diff --git a/src/wp-admin/css/colors/_tokens.scss b/src/wp-admin/css/colors/_tokens.scss new file mode 100644 index 0000000000000..785f1f47f5076 --- /dev/null +++ b/src/wp-admin/css/colors/_tokens.scss @@ -0,0 +1,212 @@ +// ========================================================================== +// WordPress Design System Tokens +// ========================================================================== +// +// These tokens are derived from the WordPress Design System in Figma: +// https://www.figma.com/design/804HN2REV2iap2ytjRQ055/WordPress-Design-System +// +// IMPORTANT: Do NOT expose these as CSS custom properties. +// Use these Sass variables to compile to static CSS values. +// The only CSS custom properties available are those in wp-base-styles: +// - --wp-admin-theme-color +// - --wp-admin-theme-color--rgb +// - --wp-admin-theme-color-darker-10 +// - --wp-admin-theme-color-darker-20 +// - --wp-admin-border-width-focus +// +// ========================================================================== + + +// -------------------------------------------------------------------------- +// Grid Units (Spacing) +// -------------------------------------------------------------------------- +// Based on 4px base unit. Use for padding, margin, and gap values. + +$grid-unit-05: 4px; // Scales/grid unit 05 +$grid-unit-10: 8px; // Scales/grid unit 10 +$grid-unit-15: 12px; // Scales/grid unit 15 +$grid-unit-20: 16px; // Scales/grid unit 20 +$grid-unit-30: 24px; // Scales/grid unit 30 +$grid-unit-40: 32px; // Scales/grid unit 40 +$grid-unit-50: 40px; // Scales/grid unit 50 +$grid-unit-60: 48px; // Scales/grid unit 60 +$grid-unit-70: 56px; // Scales/grid unit 70 + + +// -------------------------------------------------------------------------- +// Border Radius +// -------------------------------------------------------------------------- + +$radius-xs: 1px; // radius-xs +$radius-s: 2px; // radius-s - Buttons, inputs +$radius-m: 4px; // radius-m - Focus rings +$radius-l: 8px; // radius-l - Cards, dashboard widgets +$radius-30: 12px; // Radius 30 +$radius-full: 9999px; // radius-full - Pills, avatars, circles + + +// -------------------------------------------------------------------------- +// Gray Scale +// -------------------------------------------------------------------------- +// Neutral colors for backgrounds, borders, and text. + +$gray-100: #f0f0f0; // Scales/Grays/gray-100 - Page background, disabled inputs +$gray-200: #e0e0e0; // Scales/Grays/gray-200 +$gray-300: #dddddd; // Scales/Grays/gray-300 +$gray-400: #cccccc; // Scales/Grays/gray-400 - Disabled borders +$gray-600: #949494; // Scales/Grays/gray-600 - Input borders, disabled text +$gray-700: #757575; // Scales/Grays/gray-700 +$gray-800: #2f2f2f; // Scales/Grays/gray-800 +$gray-900: #1e1e1e; // Scales/Grays/gray-900 - Primary text + +$white: #ffffff; // Scales/Black & White/white + + +// -------------------------------------------------------------------------- +// Theme Colors (Static reference values) +// -------------------------------------------------------------------------- +// For actual theme color usage, use var(--wp-admin-theme-color) instead. +// These are provided for reference and for contexts where CSS vars aren't available. + +$theme-reference: #3858e9; // Scales/Theme/theme (modern scheme) +$theme-darker-10-reference: #2145e6; // Scales/Theme/theme-darker-10 +$theme-darker-20-reference: #183ad6; // Scales/Theme/theme-darker-20 +$theme-alpha-04: rgba(56, 88, 233, 0.04); // Scales/Theme/theme-alpha-04 (4% opacity) +$theme-alpha-08: rgba(56, 88, 233, 0.08); // Scales/Theme/theme-alpha-08 (8% opacity) + +$brand-9: #4465db; // Scales/brand-9 - Focus ring color (static, not theme-dependent) + + +// -------------------------------------------------------------------------- +// Semantic Colors +// -------------------------------------------------------------------------- +// Use these for notices, alerts, and status indicators. +// These are intentionally NOT theme-dependent for consistency. + +$alert-yellow: #f0b849; // Scales/Yellow/alert-yellow - Warnings +$alert-green: #4ab866; // Scales/Green/alert-green - Success +$alert-red: #cc1818; // Scales/Red/alert-red - Errors +$alert-blue: #3858e9; // Info notices (matches modern theme) + +// Background tints for notices +$alert-yellow-bg: #fef8ee; // Warning notice background +$alert-green-bg: #eff9f1; // Success notice background +$alert-red-bg: #fcf0f0; // Error notice background + +$synced-color: #7a00df; // Scales/Purple/--wp-block-synced-color + + +// -------------------------------------------------------------------------- +// Text Colors +// -------------------------------------------------------------------------- + +$text-primary: $gray-900; // Primary text color +$text-secondary: $gray-700; // Secondary text +$text-tertiary: #5d5d5d; // Alias/text/text-tertiary - Placeholder, hints +$text-disabled: $gray-600; // Disabled text + + +// -------------------------------------------------------------------------- +// Component Tokens +// -------------------------------------------------------------------------- + +// Inputs +$input-bg: $white; // Alias/bg/bg-input +$input-border-color: $gray-600; // Default input border +$input-border-color-disabled: $gray-400; +$input-bg-disabled: $gray-100; +$input-border-width-default: 1px; // Input/Default +$input-border-width-focus: 1.5px; // Input/Focus +$field-spacing-horizontal: 8px; // Alias/field-spacing-horizontal + +// Checkboxes and Radios +$checkbox-size: 16px; // Alias/checkbox +$radio-size: 16px; // Alias/radio + +// Toggles +$toggle-width: 32px; // Alias/toggle-width +$toggle-height: 16px; // Alias/toggle-height + +// Buttons +// Note: Gutenberg is transitioning to 40px as the default button size. +// The "compact" size (32px) is available for space-constrained contexts. +$button-height-default: 40px; // Default button height (next-default-40px) +$button-height-compact: 32px; // Compact button height +$button-height-small: 24px; // Small button height + +// Cards and Surfaces +$card-bg: $white; +$card-border-color: rgba(0, 0, 0, 0.1); +$card-border-width: 1px; +$card-border-radius: $radius-l; // 8px for dashboard widgets +$card-border-radius-metabox: 0; // 0 for post editor metaboxes +$card-divider-color: rgba(0, 0, 0, 0.1); + +// Card Padding Sizes +$card-padding-xs: $grid-unit-10; // 8px - xSmall cards +$card-padding-sm: $grid-unit-20; // 16px - Small cards (metaboxes, dashboard widgets) +$card-padding-md-h: $grid-unit-30; // 24px - Medium cards horizontal +$card-padding-md-v: $grid-unit-20; // 16px - Medium cards vertical +$card-padding-lg-h: $grid-unit-40; // 32px - Large cards horizontal +$card-padding-lg-v: $grid-unit-30; // 24px - Large cards vertical + +// Page Layout +$page-padding-large: 48px; // Alias/page-large +$page-padding-small: 24px; // Alias/page-small + + +// -------------------------------------------------------------------------- +// Typography Scale +// -------------------------------------------------------------------------- + +// Font Sizes +$font-size-xs: 11px; // xs - Small labels, button small +$font-size-s: 12px; // s - Body small +$font-size-m: 13px; // m - Base body text, buttons +$font-size-l: 15px; // l - Body large, heading large +$font-size-xl: 20px; // xl - Heading XL + +// Line Heights +$line-height-xs: 16px; // xs +$line-height-s: 20px; // s - Most UI elements +$line-height-m: 24px; // m - Body large + +// Font Weights +$font-weight-regular: 400; // Regular - Body text +$font-weight-medium: 500; // Medium - Headings, buttons + + +// -------------------------------------------------------------------------- +// Elevation (Box Shadows) +// -------------------------------------------------------------------------- + +$elevation-xs: + 0 4px 4px rgba(0, 0, 0, 0.01), + 0 3px 3px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.02), + 0 1px 1px rgba(0, 0, 0, 0.03); + +$elevation-s: + 0 8px 8px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.05); + +$elevation-m: + 0 16px 16px rgba(0, 0, 0, 0.02), + 0 4px 5px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); + +$elevation-l: + 0 50px 43px rgba(0, 0, 0, 0.02), + 0 30px 36px rgba(0, 0, 0, 0.04), + 0 15px 27px rgba(0, 0, 0, 0.07), + 0 5px 15px rgba(0, 0, 0, 0.08); + + +// -------------------------------------------------------------------------- +// Layout +// -------------------------------------------------------------------------- + +$modal-width-small: 384px; // Layout/Modal small +$modal-width-medium: 512px; // Layout/Modal medium +$modal-width-large: 840px; // Layout/Modal large + diff --git a/src/wp-admin/css/colors/_variables.scss b/src/wp-admin/css/colors/_variables.scss index 8a073f830e4b6..d37c2b1392f00 100644 --- a/src/wp-admin/css/colors/_variables.scss +++ b/src/wp-admin/css/colors/_variables.scss @@ -1,5 +1,8 @@ @use "sass:color"; +// Import design system tokens +@use "tokens" as *; + // assign default value to all undefined variables $scheme-name: "default" !default; @@ -15,7 +18,7 @@ $notification-color: #d54e21 !default; // global -$body-background: #f1f1f1 !default; +$body-background: $gray-100 !default; $link: #0073aa !default; $link-focus: color.adjust($link, $lightness: 10%) !default; From 24579b778fb527b87dbbb03994ec73d52384fde3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 12 Dec 2025 15:11:10 +0100 Subject: [PATCH 02/22] Admin UI: Reskin buttons to align with WordPress Design System Update all button styles to match Gutenberg's component patterns and the WordPress Design System specifications. **Sizing (aligned with Gutenberg's next-default-40px):** - Default buttons: 40px height (was 30px) - Compact buttons: 32px (new class for space-constrained contexts) - Small buttons: 24px (was 26px) - Hero buttons: 48px (was 46px) - Use min-height + line-height for accessibility with browser zoom **Visual updates:** - Border radius: 2px (was 3px) - Font weight: 500 (was 400/normal) - Transparent background for secondary/tertiary buttons **Focus states (Gutenberg-style outer ring):** - Primary: outer theme color ring + inner white ring for contrast - Secondary/Tertiary/Link: single outer theme color ring - Use var(--wp-admin-theme-color) for focus ring color **Hover/Active states:** - Secondary buttons: subtle rgba() background tint on hover/active - Use theme-color-darker-20 for hover text/border colors - Link buttons: theme-color-darker-20 on hover **Components updated:** - .button, .button-primary, .button-secondary - .button-link, .button-link-delete - .page-title-action (now uses secondary button pattern) See: https://core.trac.wordpress.org/ticket/64308 --- src/wp-admin/css/colors/_admin.scss | 144 ++++++++++----------------- src/wp-admin/css/colors/_mixins.scss | 123 ++++++++++++++++++++--- src/wp-admin/css/common.css | 10 +- src/wp-includes/css/buttons.css | 143 ++++++++++++++------------ 4 files changed, 246 insertions(+), 174 deletions(-) diff --git a/src/wp-admin/css/colors/_admin.scss b/src/wp-admin/css/colors/_admin.scss index 553037e3d30d1..97904e7e9e352 100644 --- a/src/wp-admin/css/colors/_admin.scss +++ b/src/wp-admin/css/colors/_admin.scss @@ -3,6 +3,7 @@ @forward 'variables' show $scheme-name, $base-color, $body-background, $button-color, $custom-welcome-panel, $dashboard-accent-1, $dashboard-accent-2, $dashboard-icon-background, $form-checked, $highlight-color, $icon-color, $link, $link-focus, $low-contrast-theme, $menu-bubble-text, $menu-collapse-focus-icon, $menu-collapse-text, $menu-highlight-background, $menu-highlight-icon, $menu-highlight-text, $menu-submenu-text, $menu-submenu-focus-text, $menu-submenu-background, $notification-color, $text-color; @use 'variables'; @use 'mixins'; +@use 'tokens'; /** * This function name uses British English to maintain backward compatibility, as developers @@ -37,13 +38,27 @@ span.wp-media-buttons-icon:before { color: currentColor; } -.wp-core-ui .button-link { - color: variables.$link; +/* Link button - appears as text link, no border or background */ +/* Matches Gutenberg's .is-link button variant */ +.wp-core-ui .button-link, +.wp-core-ui .button.button-link { + color: var(--wp-admin-theme-color); &:hover, - &:active, + &:active { + color: var(--wp-admin-theme-color-darker-20); + } + &:focus { - color: variables.$link-focus; + color: var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:disabled, + &[aria-disabled="true"] { + color: tokens.$gray-600; } } @@ -51,7 +66,7 @@ span.wp-media-buttons-icon:before { .media-modal .trash-attachment, .media-modal .untrash-attachment, .wp-core-ui .button-link-delete { - color: #a00; + color: tokens.$alert-red; } .media-modal .delete-attachment:hover, @@ -62,7 +77,7 @@ span.wp-media-buttons-icon:before { .media-modal .untrash-attachment:focus, .wp-core-ui .button-link-delete:hover, .wp-core-ui .button-link-delete:focus { - color: #dc3232; + color: color.adjust(tokens.$alert-red, $lightness: 10%); } /* Forms */ @@ -109,79 +124,24 @@ textarea:focus { .wp-core-ui { + /* Default button - theme color border and text (matches secondary) */ .button { - border-color: #7e8993; - color: #32373c; - } - - .button.hover, - .button:hover, - .button.focus, - .button:focus { - border-color: color.adjust(#7e8993, $lightness: -5%); - color: color.adjust(#32373c, $lightness: -5%); - } - - .button.focus, - .button:focus { - border-color: #7e8993; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: 0 0 0 1px #32373c; - } - - .button:active { - border-color: #7e8993; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: none; - } - - .button.active, - .button.active:focus, - .button.active:hover { - border-color: variables.$button-color; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: inset 0 2px 5px -3px variables.$button-color; + @include mixins.button-secondary(); } - .button.active:focus { - box-shadow: 0 0 0 1px #32373c; - } - - @if ( variables.$low-contrast-theme != "true" ) { - .button, - .button-secondary { - color: variables.$highlight-color; - border-color: variables.$highlight-color; - } - - .button.hover, - .button:hover, - .button-secondary:hover{ - border-color: color.adjust(variables.$highlight-color, $lightness: -10%); - color: color.adjust(variables.$highlight-color, $lightness: -10%); - } - - .button.focus, - .button:focus, - .button-secondary:focus { - border-color: color.adjust(variables.$highlight-color, $lightness: 10%); - color: color.adjust(variables.$highlight-color, $lightness: -20%); - box-shadow: 0 0 0 1px color.adjust(variables.$highlight-color, $lightness: 10%); - } - - .button-primary { - &:hover { - color: #fff; - } - } + /* Secondary button - same as default */ + .button-secondary { + @include mixins.button-secondary(); } + /* Primary button - theme color background */ .button-primary { @include mixins.button( variables.$button-color ); } .button-group > .button.active { - border-color: variables.$button-color; + border-color: var(--wp-admin-theme-color); + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } .wp-ui-primary { @@ -215,28 +175,34 @@ textarea:focus { /* List tables */ -@if variables.$low-contrast-theme == "true" { - .wrap .page-title-action:hover { - color: variables.$menu-text; - background-color: variables.$menu-background; - } -} @else { - .wrap .page-title-action, - .wrap .page-title-action:active { - border: 1px solid variables.$highlight-color; - color: variables.$highlight-color; - } - .wrap .page-title-action:hover { - color: color.adjust(variables.$highlight-color, $lightness: -10%); - border-color: color.adjust(variables.$highlight-color, $lightness: -10%); - } +// .page-title-action uses secondary button styling +.wrap .page-title-action { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); +} - .wrap .page-title-action:focus { - border-color: color.adjust(variables.$highlight-color, $lightness: 10%); - color: color.adjust(variables.$highlight-color, $lightness: -20%); - box-shadow: 0 0 0 1px color.adjust(variables.$highlight-color, $lightness: 10%); - } +.wrap .page-title-action:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); +} + +.wrap .page-title-action:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; +} + +.wrap .page-title-action:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; } .view-switch a.current:before { diff --git a/src/wp-admin/css/colors/_mixins.scss b/src/wp-admin/css/colors/_mixins.scss index d33cf3bb2d854..d783bf268613e 100644 --- a/src/wp-admin/css/colors/_mixins.scss +++ b/src/wp-admin/css/colors/_mixins.scss @@ -1,39 +1,132 @@ @use 'sass:color'; +@use 'tokens'; /* - * Button mixin- creates a button effect with correct - * highlights/shadows, based on a base color. + * Button mixin - creates a primary button effect. + * Uses CSS custom properties for theme color support across color schemes. */ @mixin button( $button-color, $button-text-color: #fff ) { - background: $button-color; - border-color: $button-color; + background: var(--wp-admin-theme-color); + border-color: transparent; + border-radius: tokens.$radius-s; color: $button-text-color; - &:hover, - &:focus { - background: color.adjust($button-color, $lightness: 3%); - border-color: color.adjust($button-color, $lightness: -3%); + &:hover { + background: var(--wp-admin-theme-color-darker-10); + border-color: transparent; color: $button-text-color; } &:focus { + background: var(--wp-admin-theme-color); + border-color: transparent; + color: $button-text-color; + /* Gutenberg-style focus ring: outer theme color + inset white for contrast */ box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px $button-color; + 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), + inset 0 0 0 1px tokens.$white; + /* Visible in Windows High Contrast mode */ + outline: 1px solid transparent; } &:active { - background: color.adjust($button-color, $lightness: -5%); - border-color: color.adjust($button-color, $lightness: -5%); + background: var(--wp-admin-theme-color-darker-20); + border-color: transparent; color: $button-text-color; } + &:disabled, + &.disabled { + background: tokens.$gray-100; + border-color: transparent; + color: tokens.$gray-600; + cursor: not-allowed; + } + &.active, &.active:focus, &.active:hover { - background: $button-color; + background: var(--wp-admin-theme-color-darker-10); color: $button-text-color; - border-color: color.adjust($button-color, $lightness: -15%); - box-shadow: inset 0 2px 5px -3px color.adjust($button-color, $lightness: -50%); + border-color: transparent; + box-shadow: none; + } +} + +/* + * Secondary button mixin - outlined style with theme color. + * Matches Gutenberg's .is-secondary button variant. + */ +@mixin button-secondary() { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); + + &:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-300; + color: tokens.$gray-600; + cursor: not-allowed; + } +} + +/* + * Tertiary button mixin - transparent background, gray text. + */ +@mixin button-tertiary() { + background: transparent; + border: 1px solid tokens.$gray-600; + border-radius: tokens.$radius-s; + color: tokens.$gray-900; + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: tokens.$gray-900; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(0, 0, 0, 0.1); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-400; + color: tokens.$gray-600; + cursor: not-allowed; } } diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index e062a471d7150..393787f8e6c2a 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -631,11 +631,12 @@ code { top: -3px; margin-left: 4px; border: 1px solid #2271b1; - border-radius: 3px; - background: #f6f7f7; + border-radius: 2px; + background: transparent; font-size: 13px; - font-weight: 400; - line-height: 2.15384615; + font-weight: 500; + min-height: 40px; + line-height: 2.92307692; /* 38px for 40px height */ color: #2271b1; /* use the standard color used for buttons */ padding: 0 10px; min-height: 30px; @@ -649,7 +650,6 @@ code { .wrap .add-new-h2:hover, /* deprecated */ .wrap .page-title-action:hover { - background: #f0f0f1; border-color: #0a4b78; color: #0a4b78; } diff --git a/src/wp-includes/css/buttons.css b/src/wp-includes/css/buttons.css index 5146be4274254..3eda51ae5d788 100644 --- a/src/wp-includes/css/buttons.css +++ b/src/wp-includes/css/buttons.css @@ -46,15 +46,16 @@ TABLE OF CONTENTS: display: inline-block; text-decoration: none; font-size: 13px; - line-height: 2.15384615; /* 28px */ - min-height: 30px; + font-weight: 500; + line-height: 2.92307692; /* 38px - allows 40px min-height with 1px border */ + min-height: 40px; margin: 0; - padding: 0 10px; + padding: 0 16px; cursor: pointer; border-width: 1px; border-style: solid; -webkit-appearance: none; - border-radius: 3px; + border-radius: 2px; white-space: nowrap; box-sizing: border-box; } @@ -69,26 +70,36 @@ TABLE OF CONTENTS: padding: 0; } -.wp-core-ui .button.button-large, -.wp-core-ui .button-group.button-large .button { +/* Compact size - 32px, for space-constrained contexts */ +.wp-core-ui .button.button-compact, +.wp-core-ui .button-group.button-compact .button { + line-height: 2.30769231; /* 30px - allows 32px min-height with 1px border */ min-height: 32px; - line-height: 2.30769231; /* 30px */ padding: 0 12px; } +/* Small size - 24px */ .wp-core-ui .button.button-small, .wp-core-ui .button-group.button-small .button { - min-height: 26px; - line-height: 2.18181818; /* 24px */ + line-height: 2; /* 22px - allows 24px min-height with 1px border */ + min-height: 24px; padding: 0 8px; font-size: 11px; } +/* Large size - explicit 40px (same as default, for semantic clarity) */ +.wp-core-ui .button.button-large, +.wp-core-ui .button-group.button-large .button { + line-height: 2.92307692; /* 38px - allows 40px min-height with 1px border */ + min-height: 40px; + padding: 0 16px; +} + .wp-core-ui .button.button-hero, .wp-core-ui .button-group.button-hero .button { font-size: 14px; - min-height: 46px; - line-height: 3.14285714; + line-height: 3.28571429; /* 46px - allows 48px min-height with 1px border */ + min-height: 48px; padding: 0 36px; } @@ -115,9 +126,9 @@ TABLE OF CONTENTS: .wp-core-ui .button, .wp-core-ui .button-secondary { - color: #2271b1; - border-color: #2271b1; - background: #f6f7f7; + color: #3858e9; + border-color: #3858e9; + background: transparent; vertical-align: top; } @@ -127,21 +138,21 @@ TABLE OF CONTENTS: .wp-core-ui .button.hover, .wp-core-ui .button:hover, -.wp-core-ui .button-secondary:hover{ - background: #f0f0f1; - border-color: #0a4b78; - color: #0a4b78; +.wp-core-ui .button-secondary:hover { + background: rgba(56, 88, 233, 0.04); + border-color: #183ad6; + color: #183ad6; } .wp-core-ui .button.focus, .wp-core-ui .button:focus, .wp-core-ui .button-secondary:focus { - background: #f6f7f7; - border-color: #3582c4; - color: #0a4b78; - box-shadow: 0 0 0 1px #3582c4; + background: transparent; + border-color: #3858e9; + color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; /* Only visible in Windows High Contrast mode */ - outline: 2px solid transparent; + outline: 1px solid transparent; /* Reset inherited offset from Gutenberg */ outline-offset: 0; } @@ -149,25 +160,24 @@ TABLE OF CONTENTS: /* :active state */ .wp-core-ui .button:active, .wp-core-ui .button-secondary:active { - background: #f6f7f7; - border-color: #8c8f94; + background: rgba(56, 88, 233, 0.08); + border-color: #183ad6; + color: #183ad6; box-shadow: none; } /* pressed state e.g. a selected setting */ .wp-core-ui .button.active, .wp-core-ui .button.active:hover { - background-color: #dcdcde; - color: #135e96; - border-color: #0a4b78; - box-shadow: inset 0 2px 5px -3px #0a4b78; + background-color: rgba(56, 88, 233, 0.04); + color: #3858e9; + border-color: #3858e9; + box-shadow: none; } .wp-core-ui .button.active:focus { - border-color: #3582c4; - box-shadow: - inset 0 2px 5px -3px #0a4b78, - 0 0 0 1px #3582c4; + border-color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; } .wp-core-ui .button[disabled], @@ -177,9 +187,9 @@ TABLE OF CONTENTS: .wp-core-ui .button-secondary:disabled, .wp-core-ui .button-secondary.disabled, .wp-core-ui .button-disabled { - color: #a7aaad !important; - border-color: #dcdcde !important; - background: #f6f7f7 !important; + color: #949494 !important; + border-color: #dddddd !important; + background: transparent !important; box-shadow: none !important; cursor: default; transform: none !important; @@ -201,7 +211,7 @@ TABLE OF CONTENTS: cursor: pointer; text-align: left; /* Mimics the default link style in common.css */ - color: #2271b1; + color: #3858e9; text-decoration: underline; transition-property: border, background, color; transition-duration: .05s; @@ -210,14 +220,15 @@ TABLE OF CONTENTS: .wp-core-ui .button-link:hover, .wp-core-ui .button-link:active { - color: #135e96; + color: #183ad6; } .wp-core-ui .button-link:focus { - color: #043959; - box-shadow: 0 0 0 2px #2271b1; + color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; + border-radius: 2px; /* Only visible in Windows High Contrast mode */ - outline: 2px solid transparent; + outline: 1px solid transparent; } .wp-core-ui .button-link-delete { @@ -241,35 +252,37 @@ TABLE OF CONTENTS: ---------------------------------------------------------------------------- */ .wp-core-ui .button-primary { - background: #2271b1; - border-color: #2271b1; + background: #3858e9; + border-color: #3858e9; color: #fff; text-decoration: none; text-shadow: none; } .wp-core-ui .button-primary.hover, -.wp-core-ui .button-primary:hover, -.wp-core-ui .button-primary.focus, -.wp-core-ui .button-primary:focus { - background: #135e96; - border-color: #135e96; +.wp-core-ui .button-primary:hover { + background: #2145e6; + border-color: #2145e6; color: #fff; } .wp-core-ui .button-primary.focus, .wp-core-ui .button-primary:focus { + background: #3858e9; + border-color: #3858e9; + color: #fff; box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px #2271b1; + 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9, + inset 0 0 0 1px #fff; + outline: 1px solid transparent; } .wp-core-ui .button-primary.active, .wp-core-ui .button-primary.active:hover, .wp-core-ui .button-primary.active:focus, .wp-core-ui .button-primary:active { - background: #135e96; - border-color: #135e96; + background: #183ad6; + border-color: #183ad6; box-shadow: none; color: #fff; } @@ -278,9 +291,9 @@ TABLE OF CONTENTS: .wp-core-ui .button-primary:disabled, .wp-core-ui .button-primary-disabled, .wp-core-ui .button-primary.disabled { - color: #a7aaad !important; - background: #f6f7f7 !important; - border-color: #dcdcde !important; + color: #949494 !important; + background: #f0f0f0 !important; + border-color: #f0f0f0 !important; box-shadow: none !important; text-shadow: none !important; cursor: default; @@ -309,11 +322,11 @@ TABLE OF CONTENTS: } .wp-core-ui .button-group > .button:first-child { - border-radius: 3px 0 0 3px; + border-radius: 2px 0 0 2px; } .wp-core-ui .button-group > .button:last-child { - border-radius: 0 3px 3px 0; + border-radius: 0 2px 2px 0; } .wp-core-ui .button-group > .button-primary + .button { @@ -353,7 +366,7 @@ TABLE OF CONTENTS: input#save-post, a.preview { padding: 0 14px; - line-height: 2.71428571; /* 38px */ + line-height: 2.71428571; /* 38px - allows 40px min-height with 1px border */ font-size: 14px; vertical-align: middle; min-height: 40px; @@ -366,9 +379,9 @@ TABLE OF CONTENTS: } #media-upload.wp-core-ui .button { - padding: 0 10px 1px; + padding: 0 10px; + line-height: 1.69230769; /* 22px */ min-height: 24px; - line-height: 22px; font-size: 13px; } @@ -386,8 +399,8 @@ TABLE OF CONTENTS: .wp-core-ui.wp-customizer .button { font-size: 13px; - line-height: 2.15384615; /* 28px */ - min-height: 30px; + line-height: 2.30769231; /* 30px */ + min-height: 32px; margin: 0; vertical-align: inherit; } @@ -404,9 +417,9 @@ TABLE OF CONTENTS: /* Reset responsive styles on Log in button on iframed login form */ .interim-login .button.button-large { - min-height: 30px; - line-height: 2; - padding: 0 12px 2px; + min-height: 32px; + line-height: 2.30769231; /* 30px */ + padding: 0 12px; } } From 646d99837a77009bdc06ae8ba18494e3ce5a7c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 12 Dec 2025 16:18:45 +0100 Subject: [PATCH 03/22] Admin UI: Fix theme card button sizing and visibility Update theme card buttons to work with the new design system sizing. **Button sizing:** - Use compact size (32px) for theme card buttons since they're in a space-constrained context - Set explicit min-height, line-height, and padding to match compact spec **Button visibility:** - Add white background to secondary buttons for visibility against the semi-transparent theme card overlay - Use :not(.button-primary) selector to preserve primary button styling - Adjust hover state to use #f0f0f0 background **Layout adjustments:** - Increase theme name vertical padding from 15px to 16px to accommodate taller buttons - Adjust active theme padding-right from 110px to 115px for button width - Reduce theme-actions horizontal padding from 15px to 12px See: https://core.trac.wordpress.org/ticket/64308 --- src/wp-admin/css/themes.css | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index ea62a09cf1ed1..113cec87f50e7 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -83,7 +83,7 @@ body.js .theme-browser.search-loading { font-weight: 600; height: 18px; margin: 0; - padding: 15px; + padding: 16px 15px; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); overflow: hidden; white-space: nowrap; @@ -112,9 +112,26 @@ body.js .theme-browser.search-loading { margin-right: 3px; } +/* Use compact size for space-constrained theme cards */ .theme-browser .theme .theme-actions .button { float: none; margin-left: 3px; + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px min-height */ + padding: 0 12px; +} + +/* Secondary buttons need white background for visibility on semi-transparent overlay */ +.theme-browser .theme .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):focus { + background: #fff; } /** @@ -211,7 +228,7 @@ body.js .theme-browser.search-loading { .theme-browser .theme.active .theme-name { background: #1d2327; color: #fff; - padding-right: 110px; + padding-right: 115px; font-weight: 300; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.5); } @@ -240,7 +257,7 @@ body.js .theme-browser.search-loading { top: 50%; transform: translateY(-50%); right: 0; - padding: 9px 15px; + padding: 9px 12px; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); } @@ -248,6 +265,19 @@ body.js .theme-browser.search-loading { margin-right: 0; } +/* Active theme secondary buttons need white background for visibility on dark overlay */ +.theme-browser .theme.active .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):focus { + background: #fff; +} + .theme-browser .theme .theme-author { background: #1d2327; color: #f0f0f1; From 16aa10f04dfaa8b9084f05213a9371526dc3e79b Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 19 Jan 2026 20:44:38 +0000 Subject: [PATCH 04/22] HTML API: Fix missing null-check in wp_kses_hair() refactor. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no attributes are present, `wp_kses_hair()` should return an empty array, but when the refactor was merged, the code assumed there would be attributes. An alternative fix is to use null-coalescing to iterate over an empty array. This would produce a marginally smaller function and read slightly more cleanly, but there’s no need to enter the `foreach` loop when it’s known in advance that there’s nothing over which to iterate. Developed in: https://github.com/WordPress/wordpress-develop/pull/10758 Discussed in: https://core.trac.wordpress.org/ticket/63724 Follow-up to [61467]. Props: dd32, dmsnell, jonsurrell. See: #63724. git-svn-id: https://develop.svn.wordpress.org/trunk@61499 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/kses.php | 7 ++++++- tests/phpunit/tests/kses/wpKsesHair.php | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index fd489c06c71f2..872034621f28f 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1623,6 +1623,11 @@ function wp_kses_hair( $attr, $allowed_protocols ) { $processor = new WP_HTML_Tag_Processor( "" ); $processor->next_token(); + $attribute_names = $processor->get_attribute_names_with_prefix( '' ); + if ( ! isset( $attribute_names ) ) { + return $attributes; + } + $syntax_characters = array( '&' => '&', '<' => '<', @@ -1631,7 +1636,7 @@ function wp_kses_hair( $attr, $allowed_protocols ) { '"' => '"', ); - foreach ( $processor->get_attribute_names_with_prefix( '' ) as $name ) { + foreach ( $attribute_names as $name ) { $value = $processor->get_attribute( $name ); $is_bool = true === $value; if ( is_string( $value ) && in_array( $name, $uris, true ) ) { diff --git a/tests/phpunit/tests/kses/wpKsesHair.php b/tests/phpunit/tests/kses/wpKsesHair.php index 05d573bc070bc..0ef834880cd5c 100644 --- a/tests/phpunit/tests/kses/wpKsesHair.php +++ b/tests/phpunit/tests/kses/wpKsesHair.php @@ -39,6 +39,28 @@ public function test_attribute_parsing( string $input, array $expected ) { * @return Generator */ public function data_attribute_parsing() { + yield 'empty attributes' => array( + '', + array(), + ); + + yield 'prematurely-terminated attributes' => array( + '>', + array(), + ); + + yield 'prematurely-terminated malformed attributes' => array( + 'foo>bar="baz"', + array( + 'foo' => array( + 'name' => 'foo', + 'value' => '', + 'whole' => 'foo', + 'vless' => 'y', + ), + ), + ); + yield 'single attribute with double quotes' => array( 'class="test-class"', array( From 0586dd23434e160a27342a2f728c7ca0b2e15b3e Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 20 Jan 2026 09:43:59 +0000 Subject: [PATCH 05/22] Twenty Twenty-One: Consistently use theme version when enqueueing resources. Follow-up to [49478], [49574]. Props sabernhardt. Fixes #64526. git-svn-id: https://develop.svn.wordpress.org/trunk@61500 602fd350-edb4-49c9-b593-d223f7449a82 --- .../classes/class-twenty-twenty-one-dark-mode.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php index 4aebd1bde02f5..c5643ade65da4 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php @@ -70,7 +70,7 @@ public function editor_custom_color_variables() { 'twentytwentyone-dark-mode-support-toggle', get_template_directory_uri() . '/assets/js/dark-mode-toggler.js', array(), - '1.0.0', + wp_get_theme()->get( 'Version' ), array( 'in_footer' => true ) ); @@ -78,7 +78,7 @@ public function editor_custom_color_variables() { 'twentytwentyone-editor-dark-mode-support', get_template_directory_uri() . '/assets/js/editor-dark-mode-support.js', array( 'twentytwentyone-dark-mode-support-toggle' ), - '1.0.0', + wp_get_theme()->get( 'Version' ), array( 'in_footer' => true ) ); } @@ -116,7 +116,7 @@ public function customize_controls_enqueue_scripts() { 'twentytwentyone-customize-controls', get_template_directory_uri() . '/assets/js/customize.js', array( 'customize-base', 'customize-controls', 'underscore', 'jquery', 'twentytwentyone-customize-helpers' ), - '1.0.0', + wp_get_theme()->get( 'Version' ), array( 'in_footer' => true ) ); } From bd378ade3e8b28776c77cf4af099eb81efadd80a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 20 Jan 2026 18:09:53 +0000 Subject: [PATCH 06/22] Media: Avoid warning for undefined 'file' key with image meta array in `wp_image_add_srcset_and_sizes()`. Follow-up to [35412]. Props debarghyabanerjee, sabernhardt, shanemac10, leedxw, MadtownLems, enravo, djsuperfive, westonruter. See #34430. Fixes #60480. git-svn-id: https://develop.svn.wordpress.org/trunk@61501 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 6933ad69957e2..5d350b1a18951 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -1785,7 +1785,7 @@ function wp_image_add_srcset_and_sizes( $image, $image_meta, $attachment_id ) { } // Bail early if an image has been inserted and later edited. - if ( preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) + if ( isset( $image_meta['file'] ) && preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) && ! str_contains( wp_basename( $image_src ), $img_edit_hash[0] ) ) { return $image; From 73d7a6dcd8c9139ce9e85011dbdc02325fe7914c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 20 Jan 2026 18:22:50 +0000 Subject: [PATCH 07/22] Administration: Ensure View Transitions only apply when reduced motion is not preferred. Developed in https://github.com/WordPress/wordpress-develop/pull/10762 Follow-up to [61491]. Props mukesh27, wildworks, solankisoftware, westonruter. See #64470. Fixes #64529. git-svn-id: https://develop.svn.wordpress.org/trunk@61502 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/view-transitions.css | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/css/view-transitions.css b/src/wp-admin/css/view-transitions.css index bfb556769495b..538a90b502f9e 100644 --- a/src/wp-admin/css/view-transitions.css +++ b/src/wp-admin/css/view-transitions.css @@ -1,7 +1,9 @@ -@view-transition { - navigation: auto; -} +@media (prefers-reduced-motion: no-preference) { + @view-transition { + navigation: auto; + } -#adminmenu > .menu-top { - view-transition-name: attr(id type(), none); + #adminmenu > .menu-top { + view-transition-name: attr(id type(), none); + } } From aa51e503869ddbe597f4d9031e499e502fb8356a Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 20 Jan 2026 21:04:37 +0000 Subject: [PATCH 08/22] KSES: Early-abort in wp_kses_hair() when no attributes exist. When `wp_kses_hair()` calls into the HTML API to parse an attribute string, it checks if the result might be `null` and returns early, skipping a few minor operations. It could also skip when the returned attribute count is zero. This patch adds the additional check and early-return. Developed in: https://github.com/WordPress/wordpress-develop/pull/10764 Discussed in: https://core.trac.wordpress.org/ticket/63724 Follow-up to [61499]. Props dd32, dmsnell, jonsurrell. See #63724. git-svn-id: https://develop.svn.wordpress.org/trunk@61503 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/kses.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 872034621f28f..c71453177ddc2 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1624,7 +1624,7 @@ function wp_kses_hair( $attr, $allowed_protocols ) { $processor->next_token(); $attribute_names = $processor->get_attribute_names_with_prefix( '' ); - if ( ! isset( $attribute_names ) ) { + if ( null === $attribute_names || 0 === count( $attribute_names ) ) { return $attributes; } From 6ba48dbc1a993e85e756f5f10bdb2662cd0e4c1a Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 20 Jan 2026 23:01:11 +0000 Subject: [PATCH 09/22] Build: Restore block parser in Core. The work in [61438] for Core-64393 removed the block parser classes from Core, which caused numerous scripts to fail because they were missing. Conditional checks were added in [61492] which left WordPress in an inoperable state. This patch restores the block parser in Core, in preparation for work to remove it from Gutenberg (in a separate patch). Ironically, the files were removed because the new build was copying them over from Gutenberg and the intent was to avoid having two sources of truth, but this was previously the existing mechanism, so having done nothing to the parser files would have left the status quo. This patch removes the problems originally created by removing the files. They will not be copied from Gutenberg any more and the only source of truth will be Core. Until removed from Gutenberg, because of the build changes, any changes made on the Gutenberg side will be lost unless manually copied over. Developed in: https://github.com/WordPress/wordpress-develop/pull/10761 Discussed in: https://core.trac.wordpress.org/ticket/64521 Follow-up to [61438], [61492]. Props dmsnell, mcsf, mukesh27, youknowriad. Fixes #64521. git-svn-id: https://develop.svn.wordpress.org/trunk@61504 602fd350-edb4-49c9-b593-d223f7449a82 --- .gitignore | 3 - src/wp-includes/blocks.php | 4 - .../class-wp-block-parser-block.php | 90 ++++ .../class-wp-block-parser-frame.php | 79 ++++ src/wp-includes/class-wp-block-parser.php | 404 ++++++++++++++++++ src/wp-includes/formatting.php | 5 - src/wp-settings.php | 12 +- tools/gutenberg/copy-gutenberg-build.js | 57 +-- 8 files changed, 581 insertions(+), 73 deletions(-) create mode 100644 src/wp-includes/class-wp-block-parser-block.php create mode 100644 src/wp-includes/class-wp-block-parser-frame.php create mode 100644 src/wp-includes/class-wp-block-parser.php diff --git a/.gitignore b/.gitignore index 330a92ca02c7b..3997df4c9d603 100644 --- a/.gitignore +++ b/.gitignore @@ -38,9 +38,6 @@ wp-tests-config.php /src/wp-includes/blocks/* !/src/wp-includes/blocks/index.php /src/wp-includes/build -/src/wp-includes/class-wp-block-parser.php -/src/wp-includes/class-wp-block-parser-block.php -/src/wp-includes/class-wp-block-parser-frame.php /src/wp-includes/theme.json /packagehash.txt /.gutenberg-hash diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index e2c594d7ecfc6..2a9968608106a 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -2421,10 +2421,6 @@ function parse_blocks( $content ) { */ $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); - if ( ! class_exists( $parser_class ) ) { - return array(); - } - $parser = new $parser_class(); return $parser->parse( $content ); } diff --git a/src/wp-includes/class-wp-block-parser-block.php b/src/wp-includes/class-wp-block-parser-block.php new file mode 100644 index 0000000000000..97dd687c1ffe1 --- /dev/null +++ b/src/wp-includes/class-wp-block-parser-block.php @@ -0,0 +1,90 @@ + 3 ) + * + * @since 5.0.0 + * @var array|null + */ + public $attrs; + + /** + * List of inner blocks (of this same class) + * + * @since 5.0.0 + * @var WP_Block_Parser_Block[] + */ + public $innerBlocks; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + + /** + * Resultant HTML from inside block comment delimiters + * after removing inner blocks + * + * @example "...Just testing..." -> "Just testing..." + * + * @since 5.0.0 + * @var string + */ + public $innerHTML; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + + /** + * List of string fragments and null markers where inner blocks were found + * + * @example array( + * 'innerHTML' => 'BeforeInnerAfter', + * 'innerBlocks' => array( block, block ), + * 'innerContent' => array( 'Before', null, 'Inner', null, 'After' ), + * ) + * + * @since 5.0.0 + * @var array + */ + public $innerContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + + /** + * Constructor. + * + * Will populate object properties from the provided arguments. + * + * @since 5.0.0 + * + * @param string $name Name of block. + * @param array $attrs Optional set of attributes from block comment delimiters. + * @param array $inner_blocks List of inner blocks (of this same class). + * @param string $inner_html Resultant HTML from inside block comment delimiters after removing inner blocks. + * @param array $inner_content List of string fragments and null markers where inner blocks were found. + */ + public function __construct( $name, $attrs, $inner_blocks, $inner_html, $inner_content ) { + $this->blockName = $name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + $this->attrs = $attrs; + $this->innerBlocks = $inner_blocks; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + $this->innerHTML = $inner_html; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + $this->innerContent = $inner_content; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + } +} diff --git a/src/wp-includes/class-wp-block-parser-frame.php b/src/wp-includes/class-wp-block-parser-frame.php new file mode 100644 index 0000000000000..6ab5dd3087dfb --- /dev/null +++ b/src/wp-includes/class-wp-block-parser-frame.php @@ -0,0 +1,79 @@ +block = $block; + $this->token_start = $token_start; + $this->token_length = $token_length; + $this->prev_offset = isset( $prev_offset ) ? $prev_offset : $token_start + $token_length; + $this->leading_html_start = $leading_html_start; + } +} diff --git a/src/wp-includes/class-wp-block-parser.php b/src/wp-includes/class-wp-block-parser.php new file mode 100644 index 0000000000000..bf8a59249d99d --- /dev/null +++ b/src/wp-includes/class-wp-block-parser.php @@ -0,0 +1,404 @@ +This is inside a block!" + * + * @since 5.0.0 + * @var string + */ + public $document; + + /** + * Tracks parsing progress through document + * + * @since 5.0.0 + * @var int + */ + public $offset; + + /** + * List of parsed blocks + * + * @since 5.0.0 + * @var array[] + */ + public $output; + + /** + * Stack of partially-parsed structures in memory during parse + * + * @since 5.0.0 + * @var WP_Block_Parser_Frame[] + */ + public $stack; + + /** + * Parses a document and returns a list of block structures + * + * When encountering an invalid parse will return a best-effort + * parse. In contrast to the specification parser this does not + * return an error on invalid inputs. + * + * @since 5.0.0 + * + * @param string $document Input document being parsed. + * @return array[] + */ + public function parse( $document ) { + $this->document = $document; + $this->offset = 0; + $this->output = array(); + $this->stack = array(); + + while ( $this->proceed() ) { + continue; + } + + return $this->output; + } + + /** + * Processes the next token from the input document + * and returns whether to proceed eating more tokens + * + * This is the "next step" function that essentially + * takes a token as its input and decides what to do + * with that token before descending deeper into a + * nested block tree or continuing along the document + * or breaking out of a level of nesting. + * + * @internal + * @since 5.0.0 + * @return bool + */ + public function proceed() { + $next_token = $this->next_token(); + list( $token_type, $block_name, $attrs, $start_offset, $token_length ) = $next_token; + $stack_depth = count( $this->stack ); + + // we may have some HTML soup before the next block. + $leading_html_start = $start_offset > $this->offset ? $this->offset : null; + + switch ( $token_type ) { + case 'no-more-tokens': + // if not in a block then flush output. + if ( 0 === $stack_depth ) { + $this->add_freeform(); + return false; + } + + /* + * Otherwise we have a problem + * This is an error + * + * we have options + * - treat it all as freeform text + * - assume an implicit closer (easiest when not nesting) + */ + + // for the easy case we'll assume an implicit closer. + if ( 1 === $stack_depth ) { + $this->add_block_from_stack(); + return false; + } + + /* + * for the nested case where it's more difficult we'll + * have to assume that multiple closers are missing + * and so we'll collapse the whole stack piecewise + */ + while ( 0 < count( $this->stack ) ) { + $this->add_block_from_stack(); + } + return false; + + case 'void-block': + /* + * easy case is if we stumbled upon a void block + * in the top-level of the document + */ + if ( 0 === $stack_depth ) { + if ( isset( $leading_html_start ) ) { + $this->output[] = (array) $this->freeform( + substr( + $this->document, + $leading_html_start, + $start_offset - $leading_html_start + ) + ); + } + + $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ); + $this->offset = $start_offset + $token_length; + return true; + } + + // otherwise we found an inner block. + $this->add_inner_block( + new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ), + $start_offset, + $token_length + ); + $this->offset = $start_offset + $token_length; + return true; + + case 'block-opener': + // track all newly-opened blocks on the stack. + array_push( + $this->stack, + new WP_Block_Parser_Frame( + new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ), + $start_offset, + $token_length, + $start_offset + $token_length, + $leading_html_start + ) + ); + $this->offset = $start_offset + $token_length; + return true; + + case 'block-closer': + /* + * if we're missing an opener we're in trouble + * This is an error + */ + if ( 0 === $stack_depth ) { + /* + * we have options + * - assume an implicit opener + * - assume _this_ is the opener + * - give up and close out the document + */ + $this->add_freeform(); + return false; + } + + // if we're not nesting then this is easy - close the block. + if ( 1 === $stack_depth ) { + $this->add_block_from_stack( $start_offset ); + $this->offset = $start_offset + $token_length; + return true; + } + + /* + * otherwise we're nested and we have to close out the current + * block and add it as a new innerBlock to the parent + */ + $stack_top = array_pop( $this->stack ); + $html = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset ); + $stack_top->block->innerHTML .= $html; + $stack_top->block->innerContent[] = $html; + $stack_top->prev_offset = $start_offset + $token_length; + + $this->add_inner_block( + $stack_top->block, + $stack_top->token_start, + $stack_top->token_length, + $start_offset + $token_length + ); + $this->offset = $start_offset + $token_length; + return true; + + default: + // This is an error. + $this->add_freeform(); + return false; + } + } + + /** + * Scans the document from where we last left off + * and finds the next valid token to parse if it exists + * + * Returns the type of the find: kind of find, block information, attributes + * + * @internal + * @since 5.0.0 + * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments + * @return array + */ + public function next_token() { + $matches = null; + + /* + * aye the magic + * we're using a single RegExp to tokenize the block comment delimiters + * we're also using a trick here because the only difference between a + * block opener and a block closer is the leading `/` before `wp:` (and + * a closer has no attributes). we can trap them both and process the + * match back in PHP to see which one it was. + */ + $has_match = preg_match( + '/).)*+)?}\s+)?(?P\/)?-->/s', + $this->document, + $matches, + PREG_OFFSET_CAPTURE, + $this->offset + ); + + // if we get here we probably have catastrophic backtracking or out-of-memory in the PCRE. + if ( false === $has_match ) { + return array( 'no-more-tokens', null, null, null, null ); + } + + // we have no more tokens. + if ( 0 === $has_match ) { + return array( 'no-more-tokens', null, null, null, null ); + } + + list( $match, $started_at ) = $matches[0]; + + $length = strlen( $match ); + $is_closer = isset( $matches['closer'] ) && -1 !== $matches['closer'][1]; + $is_void = isset( $matches['void'] ) && -1 !== $matches['void'][1]; + $namespace = $matches['namespace']; + $namespace = ( isset( $namespace ) && -1 !== $namespace[1] ) ? $namespace[0] : 'core/'; + $name = $namespace . $matches['name'][0]; + $has_attrs = isset( $matches['attrs'] ) && -1 !== $matches['attrs'][1]; + + /* + * Fun fact! It's not trivial in PHP to create "an empty associative array" since all arrays + * are associative arrays. If we use `array()` we get a JSON `[]` + */ + $attrs = $has_attrs + ? json_decode( $matches['attrs'][0], /* as-associative */ true ) + : array(); + + /* + * This state isn't allowed + * This is an error + */ + if ( $is_closer && ( $is_void || $has_attrs ) ) { + // we can ignore them since they don't hurt anything. + } + + if ( $is_void ) { + return array( 'void-block', $name, $attrs, $started_at, $length ); + } + + if ( $is_closer ) { + return array( 'block-closer', $name, null, $started_at, $length ); + } + + return array( 'block-opener', $name, $attrs, $started_at, $length ); + } + + /** + * Returns a new block object for freeform HTML + * + * @internal + * @since 5.0.0 + * + * @param string $inner_html HTML content of block. + * @return WP_Block_Parser_Block freeform block object. + */ + public function freeform( $inner_html ) { + return new WP_Block_Parser_Block( null, array(), array(), $inner_html, array( $inner_html ) ); + } + + /** + * Pushes a length of text from the input document + * to the output list as a freeform block. + * + * @internal + * @since 5.0.0 + * @param null $length how many bytes of document text to output. + */ + public function add_freeform( $length = null ) { + $length = $length ? $length : strlen( $this->document ) - $this->offset; + + if ( 0 === $length ) { + return; + } + + $this->output[] = (array) $this->freeform( substr( $this->document, $this->offset, $length ) ); + } + + /** + * Given a block structure from memory pushes + * a new block to the output list. + * + * @internal + * @since 5.0.0 + * @param WP_Block_Parser_Block $block The block to add to the output. + * @param int $token_start Byte offset into the document where the first token for the block starts. + * @param int $token_length Byte length of entire block from start of opening token to end of closing token. + * @param int|null $last_offset Last byte offset into document if continuing form earlier output. + */ + public function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) { + $parent = $this->stack[ count( $this->stack ) - 1 ]; + $parent->block->innerBlocks[] = (array) $block; + $html = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ); + + if ( ! empty( $html ) ) { + $parent->block->innerHTML .= $html; + $parent->block->innerContent[] = $html; + } + + $parent->block->innerContent[] = null; + $parent->prev_offset = $last_offset ? $last_offset : $token_start + $token_length; + } + + /** + * Pushes the top block from the parsing stack to the output list. + * + * @internal + * @since 5.0.0 + * @param int|null $end_offset byte offset into document for where we should stop sending text output as HTML. + */ + public function add_block_from_stack( $end_offset = null ) { + $stack_top = array_pop( $this->stack ); + $prev_offset = $stack_top->prev_offset; + + $html = isset( $end_offset ) + ? substr( $this->document, $prev_offset, $end_offset - $prev_offset ) + : substr( $this->document, $prev_offset ); + + if ( ! empty( $html ) ) { + $stack_top->block->innerHTML .= $html; + $stack_top->block->innerContent[] = $html; + } + + if ( isset( $stack_top->leading_html_start ) ) { + $this->output[] = (array) $this->freeform( + substr( + $this->document, + $stack_top->leading_html_start, + $stack_top->token_start - $stack_top->leading_html_start + ) + ); + } + + $this->output[] = (array) $stack_top->block; + } +} + +/** + * WP_Block_Parser_Block class. + * + * Required for backward compatibility in WordPress Core. + */ +require_once __DIR__ . '/class-wp-block-parser-block.php'; + +/** + * WP_Block_Parser_Frame class. + * + * Required for backward compatibility in WordPress Core. + */ +require_once __DIR__ . '/class-wp-block-parser-frame.php'; diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index b3a5a1ca135b4..f59f877775b77 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5227,11 +5227,6 @@ function wp_pre_kses_less_than_callback( $matches ) { * @return string Filtered text to run through KSES. */ function wp_pre_kses_block_attributes( $content, $allowed_html, $allowed_protocols ) { - // If the block parser isn't available, skip block attribute filtering. - if ( ! class_exists( 'WP_Block_Parser' ) ) { - return $content; - } - /* * `filter_block_content` is expected to call `wp_kses`. Temporarily remove * the filter to avoid recursion. diff --git a/src/wp-settings.php b/src/wp-settings.php index 207a69e258247..60c220100f539 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -377,15 +377,9 @@ require ABSPATH . WPINC . '/class-wp-block.php'; require ABSPATH . WPINC . '/class-wp-block-list.php'; require ABSPATH . WPINC . '/class-wp-block-metadata-registry.php'; -if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser-block.php' ) ) { - require ABSPATH . WPINC . '/class-wp-block-parser-block.php'; -} -if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser-frame.php' ) ) { - require ABSPATH . WPINC . '/class-wp-block-parser-frame.php'; -} -if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser.php' ) ) { - require ABSPATH . WPINC . '/class-wp-block-parser.php'; -} +require ABSPATH . WPINC . '/class-wp-block-parser-block.php'; +require ABSPATH . WPINC . '/class-wp-block-parser-frame.php'; +require ABSPATH . WPINC . '/class-wp-block-parser.php'; require ABSPATH . WPINC . '/class-wp-classic-to-block-menu-converter.php'; require ABSPATH . WPINC . '/class-wp-navigation-fallback.php'; require ABSPATH . WPINC . '/block-bindings.php'; diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index a66ca113e0cc2..aa30d92264bf9 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -91,22 +91,6 @@ const COPY_CONFIG = { ], }, - // PHP source files (non-block files, copied from packages) - phpSource: { - files: [ - { - // Block parser classes - package: 'block-serialization-default-parser', - files: [ - 'class-wp-block-parser.php', - 'class-wp-block-parser-block.php', - 'class-wp-block-parser-frame.php', - ], - destination: '', // Root of wp-includes - }, - ], - }, - // Theme JSON files (from Gutenberg lib directory) themeJson: { files: [ @@ -1028,38 +1012,7 @@ async function main() { ); copyBlockAssets( COPY_CONFIG.blocks ); - // 6. Copy non-block PHP source files (from packages) - console.log( '\n📦 Copying non-block PHP files...' ); - const phpSourceConfig = COPY_CONFIG.phpSource; - - for ( const fileGroup of phpSourceConfig.files ) { - const packageSrc = path.join( gutenbergPackagesDir, fileGroup.package ); - - if ( ! fs.existsSync( packageSrc ) ) { - console.log( ` ⚠️ Package not found: ${ fileGroup.package }` ); - continue; - } - - for ( const file of fileGroup.files ) { - const src = path.join( packageSrc, file ); - const dest = path.join( - wpIncludesDir, - fileGroup.destination, - file - ); - - if ( fs.existsSync( src ) ) { - fs.mkdirSync( path.dirname( dest ), { recursive: true } ); - let content = fs.readFileSync( src, 'utf8' ); - fs.writeFileSync( dest, content ); - } - } - console.log( - ` ✅ ${ fileGroup.package } (${ fileGroup.files.length } files)` - ); - } - - // 7. Copy theme JSON files (from Gutenberg lib directory) + // 6. Copy theme JSON files (from Gutenberg lib directory) console.log( '\n📦 Copying theme JSON files...' ); const themeJsonConfig = COPY_CONFIG.themeJson; const gutenbergLibDir = path.join( gutenbergDir, 'lib' ); @@ -1086,19 +1039,19 @@ async function main() { } } - // 9. Generate script-modules-packages.min.php from individual asset files + // 7. Generate script-modules-packages.min.php from individual asset files console.log( '\n📦 Generating script-modules-packages.min.php...' ); generateScriptModulesPackages(); - // 10. Generate script-loader-packages.min.php + // 8. Generate script-loader-packages.min.php console.log( '\n📦 Generating script-loader-packages.min.php...' ); generateScriptLoaderPackages(); - // 11. Generate require-dynamic-blocks.php and require-static-blocks.php + // 9. Generate require-dynamic-blocks.php and require-static-blocks.php console.log( '\n📦 Generating block registration files...' ); generateBlockRegistrationFiles(); - // 12. Generate blocks-json.php from block.json files + // 10. Generate blocks-json.php from block.json files console.log( '\n📦 Generating blocks-json.php...' ); generateBlocksJson(); From 1943f9b71e4bd48e0d41a532bb1028b5346ac7ee Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 21 Jan 2026 12:14:12 +0000 Subject: [PATCH 10/22] Docs: Remove redundant syntax from callable type declarations. The parameter name is optional in PHPStan but not supported by Psalm. While neither tools are officially supported, this removes the parse error that Psalm users otherwise see. See https://github.com/php-stubs/wordpress-stubs/issues/410 for some external discussion. Props farhad0, marian1 See #64224 git-svn-id: https://develop.svn.wordpress.org/trunk@61505 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/abilities-api/class-wp-ability.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index 4fa757e55da42..967f1641156b0 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -103,7 +103,7 @@ class WP_Ability { * The ability execute callback. * * @since 6.9.0 - * @var callable( mixed $input= ): (mixed|WP_Error) + * @var callable(mixed): (mixed|WP_Error) */ protected $execute_callback; @@ -111,7 +111,7 @@ class WP_Ability { * The optional ability permission callback. * * @since 6.9.0 - * @var callable( mixed $input= ): (bool|WP_Error) + * @var callable(mixed): (bool|WP_Error) */ protected $permission_callback; From 8a5274eda9ad41d2451f015667621ef273b0c2a5 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 21 Jan 2026 17:23:44 +0000 Subject: [PATCH 11/22] Build: Fix redirect and error handling in performance results logging. - Replace `https.request()` with native `fetch()` in `log-results.js`. - Drop www. from host name used to avoid redirects. Props mcsf. Fixes #64534. git-svn-id: https://develop.svn.wordpress.org/trunk@61507 602fd350-edb4-49c9-b593-d223f7449a82 --- .../reusable-performance-report-v2.yml | 2 +- .github/workflows/reusable-performance.yml | 2 +- tests/performance/log-results.js | 71 +++++++++---------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/.github/workflows/reusable-performance-report-v2.yml b/.github/workflows/reusable-performance-report-v2.yml index 988e60310d950..bc0174d083aa0 100644 --- a/.github/workflows/reusable-performance-report-v2.yml +++ b/.github/workflows/reusable-performance-report-v2.yml @@ -104,7 +104,7 @@ jobs: env: BASE_SHA: ${{ steps.base-sha.outputs.result }} CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} - HOST_NAME: www.codevitals.run + HOST_NAME: codevitals.run run: | if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" diff --git a/.github/workflows/reusable-performance.yml b/.github/workflows/reusable-performance.yml index f211b58890fc3..37941678978ab 100644 --- a/.github/workflows/reusable-performance.yml +++ b/.github/workflows/reusable-performance.yml @@ -347,7 +347,7 @@ jobs: env: BASE_SHA: ${{ steps.base-sha.outputs.result }} CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} - HOST_NAME: "www.codevitals.run" + HOST_NAME: "codevitals.run" run: | if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" diff --git a/tests/performance/log-results.js b/tests/performance/log-results.js index f8214b68d4eb3..a3ef4003fab6c 100644 --- a/tests/performance/log-results.js +++ b/tests/performance/log-results.js @@ -10,7 +10,6 @@ /** * External dependencies. */ -const https = require( 'https' ); const [ token, branch, hash, baseHash, date, host ] = process.argv.slice( 2 ); const { median, parseFile, accumulateValues } = require( './utils' ); @@ -82,40 +81,40 @@ for ( const { title, results } of afterStats ) { } } -const data = new TextEncoder().encode( - JSON.stringify( { - branch, - hash, - baseHash, - timestamp: date, - metrics: metrics, - baseMetrics: baseMetrics, - } ) -); - -const options = { - hostname: host, - port: 443, - path: '/api/log?token=' + token, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length, - }, -}; - -const req = https.request( options, ( res ) => { - console.log( `statusCode: ${ res.statusCode }` ); - - res.on( 'data', ( d ) => { - process.stdout.write( d ); - } ); +const data = JSON.stringify( { + branch, + hash, + baseHash, + timestamp: date, + metrics: metrics, + baseMetrics: baseMetrics, } ); -req.on( 'error', ( error ) => { - console.error( error ); - process.exit( 1 ); -} ); - -req.write( data ); -req.end(); +( async () => { + try { + const response = await fetch( + `https://${ host }/api/log?token=${ token }`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: data, + } + ); + + console.log( `statusCode: ${ response.status }` ); + + const responseText = await response.text(); + if ( responseText ) { + console.log( responseText ); + } + + if ( ! response.ok ) { + process.exit( 1 ); + } + } catch ( error ) { + console.error( error ); + process.exit( 1 ); + } +} )(); From ba7209c7f897752eb96647a63faf1a75a2274bef Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 21 Jan 2026 23:38:10 +0000 Subject: [PATCH 12/22] Docs: Add parameter descriptions for `_wp_ajax_delete_comment_response()`. Follow-up to [10204], [32652]. Props rejaulalomkhan, huzaifaalmesbah. See #64224. git-svn-id: https://develop.svn.wordpress.org/trunk@61508 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/ajax-actions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 944d0b7d985b2..9850d0543c29b 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -454,8 +454,8 @@ function wp_ajax_logged_in() { * @since 2.7.0 * @access private * - * @param int $comment_id - * @param int $delta + * @param int $comment_id Comment ID. + * @param int $delta Change in the number of total comments. Default -1. */ function _wp_ajax_delete_comment_response( $comment_id, $delta = -1 ) { $total = isset( $_POST['_total'] ) ? (int) $_POST['_total'] : 0; From d3068aeaf37fc750f0c9627b80120f1677d1339f Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 22 Jan 2026 01:11:50 +0000 Subject: [PATCH 13/22] Blocks: Ensure extract_full_block_and_advance() matches parse_blocks() The behavior of WP_Block_Processor::extract_full_block_and_advance() should produce an identical output to what parse_blocks() would return on the same substring of input. Unfortunately, when HTML spans followed inner blocks, they were being omitted in the output parse tree. This was due to an omission in the original code which would look for those blocks before advancing again after calling `extract_full_block_and_advance()` recursively. This patch adds the missing check and resolves the discrepancy. Developed in: https://github.com/WordPress/wordpress-develop/pull/10769 Discussed in: https://core.trac.wordpress.org/ticket/64538 Follow-up to [60939]. Props dmsnell, jonsurrell, jorbin. Fixes #64537. git-svn-id: https://develop.svn.wordpress.org/trunk@61509 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-block-processor.php | 11 ++ .../block-processor/wpBlockProcessor.php | 126 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/wp-includes/class-wp-block-processor.php b/src/wp-includes/class-wp-block-processor.php index 39f79b99e79a4..7b20fbf85d3bf 100644 --- a/src/wp-includes/class-wp-block-processor.php +++ b/src/wp-includes/class-wp-block-processor.php @@ -1295,6 +1295,17 @@ public function extract_full_block_and_advance(): ?array { $block['innerBlocks'][] = $inner_block; $block['innerContent'][] = null; } + + /* + * Because the parser has advanced past the closing block token, it + * may be matched on an HTML span. This needs to be processed before + * moving on to the next token at the start of the next loop iteration. + */ + if ( $this->is_html() ) { + $chunk = $this->get_html_content(); + $block['innerHTML'] .= $chunk; + $block['innerContent'][] = $chunk; + } } return $block; diff --git a/tests/phpunit/tests/block-processor/wpBlockProcessor.php b/tests/phpunit/tests/block-processor/wpBlockProcessor.php index 021288f9cb87f..6f8934e003030 100644 --- a/tests/phpunit/tests/block-processor/wpBlockProcessor.php +++ b/tests/phpunit/tests/block-processor/wpBlockProcessor.php @@ -1296,6 +1296,132 @@ public function test_scans_directly_to_requested_block_type( string $html, strin ); } + /** + * Ensures that block extraction matches the behavior of the default block parser. + * + * @ticket 64537 + * + * @dataProvider data_various_block_posts + * + * @param string $test_document An HTML document to parse as blocks. + */ + public function test_extracts_equivalent_parses_as_parse_blocks( string $test_document ) { + $processor = new WP_Block_Processor( $test_document ); + $blocks = array(); + + while ( $processor->next_block( '*' ) ) { + $blocks[] = $processor->extract_full_block_and_advance(); + } + + $this->assertSame( + parse_blocks( $test_document ), + $blocks, + 'Failed to properly parse the block structure.' + ); + } + + /** + * Data provider. + * + * @return Generator + */ + public static function data_various_block_posts() { + yield 'Empty post' => array( '' ); + + yield 'Void block' => array( '' ); + + yield 'Empty block' => array( '' ); + + yield 'Paragraph block' => array( '

Test

' ); + + yield 'Paragraph block with attributes' => array( + '

Test

', + ); + + yield 'Group with void inner' => array( + '', + ); + + /* + * @todo There is a hidden bug in here, which is possibly a problem in + * the default parser. There are HTML spans of newlines between + * these block delimiters, and without them, the parse doesn’t + * match `parse_blocks()`. However, `parse_blocks()` is inconsistent + * in its behavior. Whereas it produces an empty text chunk here, + * in the case of a void inner block it produces none. The test is + * being adjusted to step around this issue so that it can be resolved + * separately, and until it’s clear if there is an implementation issue + * with `parse_blocks()` itself. + */ + yield 'Empty columns' => array( + << + + + +HTML + , + ); + + yield 'Contentful columns' => array( + << +
    + +
  • A good point.
  • + +
+ +HTML + , + ); + + yield 'Group with mixed content' => array( + << +
+

Test

+ This is freeform. + + End + +
That’s it!
+ + +
+ +HTML + , + ); + + yield 'Nested blocks' => array( + << +
+ + + +
+ +HTML + , + ); + + yield 'Attributes on nested blocks' => array( + << + + + + + + + +HTML + , + ); + } + /** * Data provider. * From c22a748d6690e2c2908b6901ad04caa1c573e440 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 22 Jan 2026 22:32:24 +0000 Subject: [PATCH 14/22] A11y: Fix color contrast on added menu items in Customizer Changes the color on items added to menus in the Customizer from `#8c8f94` (a contrast ratio of 3.24:1) to `#646970` (a contrast ratio of 5.53:1). Props joedolson, sabernhardt, wilcosky, showravhasan, emptyopssphere, ozgursar. Fixes #64013. git-svn-id: https://develop.svn.wordpress.org/trunk@61511 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/customize-nav-menus.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/css/customize-nav-menus.css b/src/wp-admin/css/customize-nav-menus.css index 26046679ccb1d..3bf0193f02eea 100644 --- a/src/wp-admin/css/customize-nav-menus.css +++ b/src/wp-admin/css/customize-nav-menus.css @@ -672,7 +672,7 @@ #available-menu-items .menu-item-handle.item-added .item-title, #available-menu-items .menu-item-handle.item-added:hover .item-add, #available-menu-items .menu-item-handle.item-added .item-add:focus { - color: #8c8f94; + color: #646970; } #available-menu-items .menu-item-handle.item-added .item-add:before { From eefa4e8b12a4e84e0178558cec53bb028f811809 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 22 Jan 2026 23:15:34 +0000 Subject: [PATCH 15/22] Docs: Add missing parameter descriptions for `get_cli_args()`. Follow-up to [14760]. Props rejaulalomkhan, huzaifaalmesbah, westonruter, SergeyBiryukov. See #64224. git-svn-id: https://develop.svn.wordpress.org/trunk@61512 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-importer.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/class-wp-importer.php b/src/wp-admin/includes/class-wp-importer.php index 16b08bf42b15a..883be2c218718 100644 --- a/src/wp-admin/includes/class-wp-importer.php +++ b/src/wp-admin/includes/class-wp-importer.php @@ -289,9 +289,10 @@ public function stop_the_insanity() { * Returns value of command line params. * Exits when a required param is not set. * - * @param string $param - * @param bool $required - * @return mixed + * @param string $param The parameter name to retrieve. + * @param bool $required Optional. Whether the parameter is required. Default false. + * @return string|true|null|never The parameter value or true if found, null otherwise. + * The function exits when a required parameter is missing. */ function get_cli_args( $param, $required = false ) { $args = $_SERVER['argv']; From ea3308797b439c19ddc90bb0d6727441f9a9013b Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Fri, 23 Jan 2026 01:54:48 +0000 Subject: [PATCH 16/22] Editor: Grid block responsive enhancements. Adds styles for responsive grid layouts and fixes a block gap bug and a max column width bug. Props isabel_brison, aaronrobertshaw. Fixes #64532. git-svn-id: https://develop.svn.wordpress.org/trunk@61513 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-supports/layout.php | 101 ++++++++++++++---- .../blocks/fixtures/core__columns.server.html | 2 +- .../core__columns__deprecated.server.html | 2 +- tests/phpunit/tests/block-supports/layout.php | 36 ++++--- 4 files changed, 106 insertions(+), 35 deletions(-) diff --git a/src/wp-includes/block-supports/layout.php b/src/wp-includes/block-supports/layout.php index fb46b78ab8f24..bd1badc7a6cd6 100644 --- a/src/wp-includes/block-supports/layout.php +++ b/src/wp-includes/block-supports/layout.php @@ -233,7 +233,7 @@ function wp_register_layout_support( $block_type ) { * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false. * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null. * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false. - * @param string $fallback_gap_value Optional. The block gap value to apply. Default '0.5em'. + * @param string|array $fallback_gap_value Optional. The block gap value to apply. If it's an array expected properties are "top" and/or "left". Default '0.5em'. * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null. * @return string CSS styles on success. Else, empty string. */ @@ -427,7 +427,12 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false foreach ( $gap_sides as $gap_side ) { $process_value = $gap_value; if ( is_array( $gap_value ) ) { - $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value; + if ( is_array( $fallback_gap_value ) ) { + $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value ); + } else { + $fallback_value = $fallback_gap_value; + } + $process_value = $gap_value[ $gap_side ] ?? $fallback_value; } // Get spacing CSS variable from preset value if provided. if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) { @@ -490,21 +495,14 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false } } } elseif ( 'grid' === $layout_type ) { - if ( ! empty( $layout['columnCount'] ) ) { - $layout_styles[] = array( - 'selector' => $selector, - 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ), - ); + /* + * If the gap value is an array, we use the "left" value because it represents the vertical gap, which + * is the relevant one for computation of responsive grid columns. + */ + if ( is_array( $fallback_gap_value ) ) { + $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value ); } else { - $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem'; - - $layout_styles[] = array( - 'selector' => $selector, - 'declarations' => array( - 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))', - 'container-type' => 'inline-size', - ), - ); + $responsive_gap_value = $fallback_gap_value; } if ( $has_block_gap_support && isset( $gap_value ) ) { @@ -514,7 +512,12 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false foreach ( $gap_sides as $gap_side ) { $process_value = $gap_value; if ( is_array( $gap_value ) ) { - $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value; + if ( is_array( $fallback_gap_value ) ) { + $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value ); + } else { + $fallback_value = $fallback_gap_value; + } + $process_value = $gap_value[ $gap_side ] ?? $fallback_value; } // Get spacing CSS variable from preset value if provided. if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) { @@ -524,14 +527,58 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false } $combined_gap_value .= "$process_value "; } - $gap_value = trim( $combined_gap_value ); + $gap_value = trim( $combined_gap_value ); + $responsive_gap_value = $gap_value; + } - if ( null !== $gap_value && ! $should_skip_gap_serialization ) { + // Ensure 0 values have a unit so they work in calc(). + if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) { + $responsive_gap_value = '0px'; + } + + if ( ! empty( $layout['columnCount'] ) && ! empty( $layout['minimumColumnWidth'] ) ) { + $max_value = 'max(min(' . $layout['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout['columnCount'] . ' - 1))) /' . $layout['columnCount'] . ')'; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( + 'grid-template-columns' => 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))', + 'container-type' => 'inline-size', + ), + ); + if ( ! empty( $layout['rowCount'] ) ) { $layout_styles[] = array( 'selector' => $selector, - 'declarations' => array( 'gap' => $gap_value ), + 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ), ); } + } elseif ( ! empty( $layout['columnCount'] ) ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ), + ); + if ( ! empty( $layout['rowCount'] ) ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ), + ); + } + } else { + $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem'; + + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( + 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))', + 'container-type' => 'inline-size', + ), + ); + } + + if ( $has_block_gap_support && null !== $gap_value && ! $should_skip_gap_serialization ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'gap' => $gap_value ), + ); } } @@ -568,6 +615,8 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false * @return string Filtered block content. */ function wp_render_layout_support_flag( $block_content, $block ) { + static $global_styles = null; + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); $block_supports_layout = block_has_support( $block_type, 'layout', false ) || block_has_support( $block_type, '__experimentalLayout', false ); $child_layout = $block['attrs']['style']['layout'] ?? null; @@ -804,6 +853,18 @@ function wp_render_layout_support_flag( $block_content, $block ) { $block_gap = $global_settings['spacing']['blockGap'] ?? null; $has_block_gap_support = isset( $block_gap ); + // Get default blockGap value from global styles for use in layouts like grid. + // Check block-specific styles first, then fall back to root styles. + $block_name = $block['blockName'] ?? ''; + if ( null === $global_styles ) { + $global_styles = wp_get_global_styles(); + } + $global_block_gap_value = $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? ( $global_styles['spacing']['blockGap'] ?? null ); + + if ( null !== $global_block_gap_value ) { + $fallback_gap_value = $global_block_gap_value; + } + /* * Generates a unique ID based on all the data required to obtain the * corresponding layout style. Keeps the CSS class names the same diff --git a/tests/phpunit/data/blocks/fixtures/core__columns.server.html b/tests/phpunit/data/blocks/fixtures/core__columns.server.html index 02d855cbd6c38..5b5faf4f3d7a4 100644 --- a/tests/phpunit/data/blocks/fixtures/core__columns.server.html +++ b/tests/phpunit/data/blocks/fixtures/core__columns.server.html @@ -1,5 +1,5 @@ -
+
diff --git a/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html b/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html index 6b695d15963de..b19349744dfbc 100644 --- a/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html +++ b/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html @@ -1,5 +1,5 @@ -
+

Column One, Paragraph One

diff --git a/tests/phpunit/tests/block-supports/layout.php b/tests/phpunit/tests/block-supports/layout.php index 3b71b2c4b7482..8661077df8662 100644 --- a/tests/phpunit/tests/block-supports/layout.php +++ b/tests/phpunit/tests/block-supports/layout.php @@ -293,7 +293,7 @@ public function data_layout_support_flag_renders_classnames_on_wrapper() { ), ), ), - 'expected_output' => '
', + 'expected_output' => '
', ), 'single wrapper block layout with grid type' => array( 'args' => array( @@ -312,7 +312,7 @@ public function data_layout_support_flag_renders_classnames_on_wrapper() { ), ), ), - 'expected_output' => '
', + 'expected_output' => '
', ), 'skip classname output if block does not support layout and there are no child layout classes to be output' => array( 'args' => array( @@ -542,9 +542,19 @@ public function test_layout_support_flag_renders_consistent_container_hash( $blo $processor = new WP_HTML_Tag_Processor( $output ); $processor->next_tag(); - $this->assertTrue( - $processor->has_class( $expected_class ), - "Expected class '$expected_class' not found in the rendered output, probably because of a different hash." + // Extract the actual container class from the output for better error messages. + $actual_class = ''; + foreach ( $processor->class_list() as $class_name ) { + if ( str_starts_with( $class_name, 'wp-container-core-group-is-layout-' ) ) { + $actual_class = $class_name; + break; + } + } + + $this->assertEquals( + $expected_class, + $actual_class, + 'Expected class not found in the rendered output, probably because of a different hash.' ); } @@ -566,7 +576,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { ), ), ), - 'expected_class' => 'wp-container-core-group-is-layout-c5c7d83f', + 'expected_class' => 'wp-container-core-group-is-layout-a6248535', ), 'default type block gap 24px' => array( 'block_attributes' => array( @@ -579,7 +589,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { ), ), ), - 'expected_class' => 'wp-container-core-group-is-layout-634f0b9d', + 'expected_class' => 'wp-container-core-group-is-layout-61b496ee', ), 'constrained type justified left' => array( 'block_attributes' => array( @@ -588,7 +598,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'justifyContent' => 'left', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-12dd3699', + 'expected_class' => 'wp-container-core-group-is-layout-54d22900', ), 'constrained type justified right' => array( 'block_attributes' => array( @@ -597,7 +607,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'justifyContent' => 'right', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-f1f2ed93', + 'expected_class' => 'wp-container-core-group-is-layout-2910ada7', ), 'flex type horizontal' => array( 'block_attributes' => array( @@ -607,7 +617,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'flexWrap' => 'nowrap', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-2487dcaa', + 'expected_class' => 'wp-container-core-group-is-layout-f5d79bea', ), 'flex type vertical' => array( 'block_attributes' => array( @@ -616,7 +626,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'orientation' => 'vertical', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-fe9cc265', + 'expected_class' => 'wp-container-core-group-is-layout-2c90304e', ), 'grid type' => array( 'block_attributes' => array( @@ -624,7 +634,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'type' => 'grid', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-478b6e6b', + 'expected_class' => 'wp-container-core-group-is-layout-5a23bf8e', ), 'grid type 3 columns' => array( 'block_attributes' => array( @@ -633,7 +643,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'columnCount' => 3, ), ), - 'expected_class' => 'wp-container-core-group-is-layout-d3b710ac', + 'expected_class' => 'wp-container-core-group-is-layout-cda6dc4f', ), ); } From 340b14298637bb1a48c3ab3479a88b8018e7b2db Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Fri, 23 Jan 2026 02:53:27 +0000 Subject: [PATCH 17/22] A11y: Use pointer cursor on webkit field cancellation buttons. Webkit-based browsers add a cancel button to search inputs. This input uses the default cursor standard to buttons and inputs, but the WordPress admin applies the `pointer` cursor to buttons and inputs. Apply the WordPress standard pointer to this pseudoelement for UI consistency. Props sumitsingh, manhphucofficial, joedolson, sabernhardt, dhruvang21. Fixes #64382. git-svn-id: https://develop.svn.wordpress.org/trunk@61514 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/forms.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 60285a9adc178..719be1aad9ba1 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -209,6 +209,10 @@ input[type="search"]::-webkit-search-decoration { display: none; } +input[type="search"]::-webkit-search-cancel-button { + cursor: pointer; +} + .wp-admin input[type="file"] { padding: 3px 0; cursor: pointer; From ca4981676fdd425cef6ef97fe2d62aec5bdf9ec8 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Fri, 23 Jan 2026 02:59:07 +0000 Subject: [PATCH 18/22] Bundled Themes: A11y: Remove auto-focus in Twenty Ten. Remove the autofocus script in Twenty Ten that forced a bypass of the navigation, main heading, and explanation on the 404 error page. Props sabernhardt, ravichudasama01, joedolson. Fixes #64064. git-svn-id: https://develop.svn.wordpress.org/trunk@61515 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentyten/404.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/wp-content/themes/twentyten/404.php b/src/wp-content/themes/twentyten/404.php index 64888d9f12134..8b7b13695a000 100644 --- a/src/wp-content/themes/twentyten/404.php +++ b/src/wp-content/themes/twentyten/404.php @@ -22,9 +22,5 @@
- From b7cb91c76ce69bffb1f2b6fc0ecc07a8ed6a0c9d Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Fri, 23 Jan 2026 05:56:40 +0000 Subject: [PATCH 19/22] Editor: guard against non-string values in style engine. Checks that the value passed to add_declaration is a string to prevent fatal errors due to malformed block attributes. Props andrewserong. Fixes #64545. git-svn-id: https://develop.svn.wordpress.org/trunk@61516 602fd350-edb4-49c9-b593-d223f7449a82 --- ...class-wp-style-engine-css-declarations.php | 5 ++++ .../wpStyleEngineCssDeclarations.php | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php b/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php index fbcb54b73aa3e..796ea86c207a4 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php @@ -59,6 +59,11 @@ public function add_declaration( $property, $value ) { return $this; } + // Bail early if value is not a string. Prevents fatal errors from malformed block markup. + if ( ! is_string( $value ) ) { + return $this; + } + // Trims the value. If empty, bail early. $value = trim( $value ); if ( '' === $value ) { diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php index b1f6203e6bb00..b510edcbfd0cb 100644 --- a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php @@ -290,4 +290,32 @@ public function test_should_remove_multiple_declarations() { 'Output after removing "color" and "margin" declarations via `remove_declarations()` does not match expectations' ); } + + /** + * Tests that non-string values are rejected without causing fatal errors. + * + * @ticket 64545 + * + * @covers ::add_declaration + */ + public function test_should_reject_non_string_values() { + $css_declarations = new WP_Style_Engine_CSS_Declarations(); + + // Add valid string value first. + $css_declarations->add_declaration( 'color', 'red' ); + + // Try to add array value - should be silently rejected. + $css_declarations->add_declaration( 'padding-margin', array( 'top' => '10px' ) ); + + // Try to add other non-string values. + $css_declarations->add_declaration( 'font-size', 123 ); + $css_declarations->add_declaration( 'margin', null ); + + // Only the valid string value should be stored. + $this->assertSame( + array( 'color' => 'red' ), + $css_declarations->get_declarations(), + 'Non-string values should be rejected without causing errors.' + ); + } } From 23472a1a8f835f791eaa1b6c2ac9ca6f9ec0ccae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 12 Dec 2025 14:12:15 +0100 Subject: [PATCH 20/22] Admin UI: Add design system tokens for admin reskin Introduces _tokens.scss with Sass variables derived from the WordPress Design System in Figma. These tokens provide consistent values for: - Spacing (4px grid units) - Border radius - Gray scale - Semantic colors (alerts/notices) - Typography scale - Elevation (box shadows) - Component sizing (buttons, inputs, checkboxes) The tokens are imported into _variables.scss and can be used across all admin stylesheets compiled via Sass. Note: These are Sass-only variables. No new CSS custom properties are exposed to maintain backward compatibility. The only CSS custom properties available remain those from wp-base-styles. Part of the WordPress 7.0 admin visual reskin initiative. See: https://core.trac.wordpress.org/ticket/64308 --- src/wp-admin/css/colors/_tokens.scss | 212 ++++++++++++++++++++++++ src/wp-admin/css/colors/_variables.scss | 5 +- 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/wp-admin/css/colors/_tokens.scss diff --git a/src/wp-admin/css/colors/_tokens.scss b/src/wp-admin/css/colors/_tokens.scss new file mode 100644 index 0000000000000..785f1f47f5076 --- /dev/null +++ b/src/wp-admin/css/colors/_tokens.scss @@ -0,0 +1,212 @@ +// ========================================================================== +// WordPress Design System Tokens +// ========================================================================== +// +// These tokens are derived from the WordPress Design System in Figma: +// https://www.figma.com/design/804HN2REV2iap2ytjRQ055/WordPress-Design-System +// +// IMPORTANT: Do NOT expose these as CSS custom properties. +// Use these Sass variables to compile to static CSS values. +// The only CSS custom properties available are those in wp-base-styles: +// - --wp-admin-theme-color +// - --wp-admin-theme-color--rgb +// - --wp-admin-theme-color-darker-10 +// - --wp-admin-theme-color-darker-20 +// - --wp-admin-border-width-focus +// +// ========================================================================== + + +// -------------------------------------------------------------------------- +// Grid Units (Spacing) +// -------------------------------------------------------------------------- +// Based on 4px base unit. Use for padding, margin, and gap values. + +$grid-unit-05: 4px; // Scales/grid unit 05 +$grid-unit-10: 8px; // Scales/grid unit 10 +$grid-unit-15: 12px; // Scales/grid unit 15 +$grid-unit-20: 16px; // Scales/grid unit 20 +$grid-unit-30: 24px; // Scales/grid unit 30 +$grid-unit-40: 32px; // Scales/grid unit 40 +$grid-unit-50: 40px; // Scales/grid unit 50 +$grid-unit-60: 48px; // Scales/grid unit 60 +$grid-unit-70: 56px; // Scales/grid unit 70 + + +// -------------------------------------------------------------------------- +// Border Radius +// -------------------------------------------------------------------------- + +$radius-xs: 1px; // radius-xs +$radius-s: 2px; // radius-s - Buttons, inputs +$radius-m: 4px; // radius-m - Focus rings +$radius-l: 8px; // radius-l - Cards, dashboard widgets +$radius-30: 12px; // Radius 30 +$radius-full: 9999px; // radius-full - Pills, avatars, circles + + +// -------------------------------------------------------------------------- +// Gray Scale +// -------------------------------------------------------------------------- +// Neutral colors for backgrounds, borders, and text. + +$gray-100: #f0f0f0; // Scales/Grays/gray-100 - Page background, disabled inputs +$gray-200: #e0e0e0; // Scales/Grays/gray-200 +$gray-300: #dddddd; // Scales/Grays/gray-300 +$gray-400: #cccccc; // Scales/Grays/gray-400 - Disabled borders +$gray-600: #949494; // Scales/Grays/gray-600 - Input borders, disabled text +$gray-700: #757575; // Scales/Grays/gray-700 +$gray-800: #2f2f2f; // Scales/Grays/gray-800 +$gray-900: #1e1e1e; // Scales/Grays/gray-900 - Primary text + +$white: #ffffff; // Scales/Black & White/white + + +// -------------------------------------------------------------------------- +// Theme Colors (Static reference values) +// -------------------------------------------------------------------------- +// For actual theme color usage, use var(--wp-admin-theme-color) instead. +// These are provided for reference and for contexts where CSS vars aren't available. + +$theme-reference: #3858e9; // Scales/Theme/theme (modern scheme) +$theme-darker-10-reference: #2145e6; // Scales/Theme/theme-darker-10 +$theme-darker-20-reference: #183ad6; // Scales/Theme/theme-darker-20 +$theme-alpha-04: rgba(56, 88, 233, 0.04); // Scales/Theme/theme-alpha-04 (4% opacity) +$theme-alpha-08: rgba(56, 88, 233, 0.08); // Scales/Theme/theme-alpha-08 (8% opacity) + +$brand-9: #4465db; // Scales/brand-9 - Focus ring color (static, not theme-dependent) + + +// -------------------------------------------------------------------------- +// Semantic Colors +// -------------------------------------------------------------------------- +// Use these for notices, alerts, and status indicators. +// These are intentionally NOT theme-dependent for consistency. + +$alert-yellow: #f0b849; // Scales/Yellow/alert-yellow - Warnings +$alert-green: #4ab866; // Scales/Green/alert-green - Success +$alert-red: #cc1818; // Scales/Red/alert-red - Errors +$alert-blue: #3858e9; // Info notices (matches modern theme) + +// Background tints for notices +$alert-yellow-bg: #fef8ee; // Warning notice background +$alert-green-bg: #eff9f1; // Success notice background +$alert-red-bg: #fcf0f0; // Error notice background + +$synced-color: #7a00df; // Scales/Purple/--wp-block-synced-color + + +// -------------------------------------------------------------------------- +// Text Colors +// -------------------------------------------------------------------------- + +$text-primary: $gray-900; // Primary text color +$text-secondary: $gray-700; // Secondary text +$text-tertiary: #5d5d5d; // Alias/text/text-tertiary - Placeholder, hints +$text-disabled: $gray-600; // Disabled text + + +// -------------------------------------------------------------------------- +// Component Tokens +// -------------------------------------------------------------------------- + +// Inputs +$input-bg: $white; // Alias/bg/bg-input +$input-border-color: $gray-600; // Default input border +$input-border-color-disabled: $gray-400; +$input-bg-disabled: $gray-100; +$input-border-width-default: 1px; // Input/Default +$input-border-width-focus: 1.5px; // Input/Focus +$field-spacing-horizontal: 8px; // Alias/field-spacing-horizontal + +// Checkboxes and Radios +$checkbox-size: 16px; // Alias/checkbox +$radio-size: 16px; // Alias/radio + +// Toggles +$toggle-width: 32px; // Alias/toggle-width +$toggle-height: 16px; // Alias/toggle-height + +// Buttons +// Note: Gutenberg is transitioning to 40px as the default button size. +// The "compact" size (32px) is available for space-constrained contexts. +$button-height-default: 40px; // Default button height (next-default-40px) +$button-height-compact: 32px; // Compact button height +$button-height-small: 24px; // Small button height + +// Cards and Surfaces +$card-bg: $white; +$card-border-color: rgba(0, 0, 0, 0.1); +$card-border-width: 1px; +$card-border-radius: $radius-l; // 8px for dashboard widgets +$card-border-radius-metabox: 0; // 0 for post editor metaboxes +$card-divider-color: rgba(0, 0, 0, 0.1); + +// Card Padding Sizes +$card-padding-xs: $grid-unit-10; // 8px - xSmall cards +$card-padding-sm: $grid-unit-20; // 16px - Small cards (metaboxes, dashboard widgets) +$card-padding-md-h: $grid-unit-30; // 24px - Medium cards horizontal +$card-padding-md-v: $grid-unit-20; // 16px - Medium cards vertical +$card-padding-lg-h: $grid-unit-40; // 32px - Large cards horizontal +$card-padding-lg-v: $grid-unit-30; // 24px - Large cards vertical + +// Page Layout +$page-padding-large: 48px; // Alias/page-large +$page-padding-small: 24px; // Alias/page-small + + +// -------------------------------------------------------------------------- +// Typography Scale +// -------------------------------------------------------------------------- + +// Font Sizes +$font-size-xs: 11px; // xs - Small labels, button small +$font-size-s: 12px; // s - Body small +$font-size-m: 13px; // m - Base body text, buttons +$font-size-l: 15px; // l - Body large, heading large +$font-size-xl: 20px; // xl - Heading XL + +// Line Heights +$line-height-xs: 16px; // xs +$line-height-s: 20px; // s - Most UI elements +$line-height-m: 24px; // m - Body large + +// Font Weights +$font-weight-regular: 400; // Regular - Body text +$font-weight-medium: 500; // Medium - Headings, buttons + + +// -------------------------------------------------------------------------- +// Elevation (Box Shadows) +// -------------------------------------------------------------------------- + +$elevation-xs: + 0 4px 4px rgba(0, 0, 0, 0.01), + 0 3px 3px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.02), + 0 1px 1px rgba(0, 0, 0, 0.03); + +$elevation-s: + 0 8px 8px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.05); + +$elevation-m: + 0 16px 16px rgba(0, 0, 0, 0.02), + 0 4px 5px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); + +$elevation-l: + 0 50px 43px rgba(0, 0, 0, 0.02), + 0 30px 36px rgba(0, 0, 0, 0.04), + 0 15px 27px rgba(0, 0, 0, 0.07), + 0 5px 15px rgba(0, 0, 0, 0.08); + + +// -------------------------------------------------------------------------- +// Layout +// -------------------------------------------------------------------------- + +$modal-width-small: 384px; // Layout/Modal small +$modal-width-medium: 512px; // Layout/Modal medium +$modal-width-large: 840px; // Layout/Modal large + diff --git a/src/wp-admin/css/colors/_variables.scss b/src/wp-admin/css/colors/_variables.scss index 8a073f830e4b6..d37c2b1392f00 100644 --- a/src/wp-admin/css/colors/_variables.scss +++ b/src/wp-admin/css/colors/_variables.scss @@ -1,5 +1,8 @@ @use "sass:color"; +// Import design system tokens +@use "tokens" as *; + // assign default value to all undefined variables $scheme-name: "default" !default; @@ -15,7 +18,7 @@ $notification-color: #d54e21 !default; // global -$body-background: #f1f1f1 !default; +$body-background: $gray-100 !default; $link: #0073aa !default; $link-focus: color.adjust($link, $lightness: 10%) !default; From a91510a7857cbb345a1006ec8b035fd4865626f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 12 Dec 2025 15:11:10 +0100 Subject: [PATCH 21/22] Admin UI: Reskin buttons to align with WordPress Design System Update all button styles to match Gutenberg's component patterns and the WordPress Design System specifications. **Sizing (aligned with Gutenberg's next-default-40px):** - Default buttons: 40px height (was 30px) - Compact buttons: 32px (new class for space-constrained contexts) - Small buttons: 24px (was 26px) - Hero buttons: 48px (was 46px) - Use min-height + line-height for accessibility with browser zoom **Visual updates:** - Border radius: 2px (was 3px) - Font weight: 500 (was 400/normal) - Transparent background for secondary/tertiary buttons **Focus states (Gutenberg-style outer ring):** - Primary: outer theme color ring + inner white ring for contrast - Secondary/Tertiary/Link: single outer theme color ring - Use var(--wp-admin-theme-color) for focus ring color **Hover/Active states:** - Secondary buttons: subtle rgba() background tint on hover/active - Use theme-color-darker-20 for hover text/border colors - Link buttons: theme-color-darker-20 on hover **Components updated:** - .button, .button-primary, .button-secondary - .button-link, .button-link-delete - .page-title-action (now uses secondary button pattern) See: https://core.trac.wordpress.org/ticket/64308 --- src/wp-admin/css/colors/_admin.scss | 144 ++++++++++----------------- src/wp-admin/css/colors/_mixins.scss | 123 ++++++++++++++++++++--- src/wp-admin/css/common.css | 10 +- src/wp-includes/css/buttons.css | 143 ++++++++++++++------------ 4 files changed, 246 insertions(+), 174 deletions(-) diff --git a/src/wp-admin/css/colors/_admin.scss b/src/wp-admin/css/colors/_admin.scss index 553037e3d30d1..97904e7e9e352 100644 --- a/src/wp-admin/css/colors/_admin.scss +++ b/src/wp-admin/css/colors/_admin.scss @@ -3,6 +3,7 @@ @forward 'variables' show $scheme-name, $base-color, $body-background, $button-color, $custom-welcome-panel, $dashboard-accent-1, $dashboard-accent-2, $dashboard-icon-background, $form-checked, $highlight-color, $icon-color, $link, $link-focus, $low-contrast-theme, $menu-bubble-text, $menu-collapse-focus-icon, $menu-collapse-text, $menu-highlight-background, $menu-highlight-icon, $menu-highlight-text, $menu-submenu-text, $menu-submenu-focus-text, $menu-submenu-background, $notification-color, $text-color; @use 'variables'; @use 'mixins'; +@use 'tokens'; /** * This function name uses British English to maintain backward compatibility, as developers @@ -37,13 +38,27 @@ span.wp-media-buttons-icon:before { color: currentColor; } -.wp-core-ui .button-link { - color: variables.$link; +/* Link button - appears as text link, no border or background */ +/* Matches Gutenberg's .is-link button variant */ +.wp-core-ui .button-link, +.wp-core-ui .button.button-link { + color: var(--wp-admin-theme-color); &:hover, - &:active, + &:active { + color: var(--wp-admin-theme-color-darker-20); + } + &:focus { - color: variables.$link-focus; + color: var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:disabled, + &[aria-disabled="true"] { + color: tokens.$gray-600; } } @@ -51,7 +66,7 @@ span.wp-media-buttons-icon:before { .media-modal .trash-attachment, .media-modal .untrash-attachment, .wp-core-ui .button-link-delete { - color: #a00; + color: tokens.$alert-red; } .media-modal .delete-attachment:hover, @@ -62,7 +77,7 @@ span.wp-media-buttons-icon:before { .media-modal .untrash-attachment:focus, .wp-core-ui .button-link-delete:hover, .wp-core-ui .button-link-delete:focus { - color: #dc3232; + color: color.adjust(tokens.$alert-red, $lightness: 10%); } /* Forms */ @@ -109,79 +124,24 @@ textarea:focus { .wp-core-ui { + /* Default button - theme color border and text (matches secondary) */ .button { - border-color: #7e8993; - color: #32373c; - } - - .button.hover, - .button:hover, - .button.focus, - .button:focus { - border-color: color.adjust(#7e8993, $lightness: -5%); - color: color.adjust(#32373c, $lightness: -5%); - } - - .button.focus, - .button:focus { - border-color: #7e8993; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: 0 0 0 1px #32373c; - } - - .button:active { - border-color: #7e8993; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: none; - } - - .button.active, - .button.active:focus, - .button.active:hover { - border-color: variables.$button-color; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: inset 0 2px 5px -3px variables.$button-color; + @include mixins.button-secondary(); } - .button.active:focus { - box-shadow: 0 0 0 1px #32373c; - } - - @if ( variables.$low-contrast-theme != "true" ) { - .button, - .button-secondary { - color: variables.$highlight-color; - border-color: variables.$highlight-color; - } - - .button.hover, - .button:hover, - .button-secondary:hover{ - border-color: color.adjust(variables.$highlight-color, $lightness: -10%); - color: color.adjust(variables.$highlight-color, $lightness: -10%); - } - - .button.focus, - .button:focus, - .button-secondary:focus { - border-color: color.adjust(variables.$highlight-color, $lightness: 10%); - color: color.adjust(variables.$highlight-color, $lightness: -20%); - box-shadow: 0 0 0 1px color.adjust(variables.$highlight-color, $lightness: 10%); - } - - .button-primary { - &:hover { - color: #fff; - } - } + /* Secondary button - same as default */ + .button-secondary { + @include mixins.button-secondary(); } + /* Primary button - theme color background */ .button-primary { @include mixins.button( variables.$button-color ); } .button-group > .button.active { - border-color: variables.$button-color; + border-color: var(--wp-admin-theme-color); + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } .wp-ui-primary { @@ -215,28 +175,34 @@ textarea:focus { /* List tables */ -@if variables.$low-contrast-theme == "true" { - .wrap .page-title-action:hover { - color: variables.$menu-text; - background-color: variables.$menu-background; - } -} @else { - .wrap .page-title-action, - .wrap .page-title-action:active { - border: 1px solid variables.$highlight-color; - color: variables.$highlight-color; - } - .wrap .page-title-action:hover { - color: color.adjust(variables.$highlight-color, $lightness: -10%); - border-color: color.adjust(variables.$highlight-color, $lightness: -10%); - } +// .page-title-action uses secondary button styling +.wrap .page-title-action { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); +} - .wrap .page-title-action:focus { - border-color: color.adjust(variables.$highlight-color, $lightness: 10%); - color: color.adjust(variables.$highlight-color, $lightness: -20%); - box-shadow: 0 0 0 1px color.adjust(variables.$highlight-color, $lightness: 10%); - } +.wrap .page-title-action:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); +} + +.wrap .page-title-action:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; +} + +.wrap .page-title-action:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; } .view-switch a.current:before { diff --git a/src/wp-admin/css/colors/_mixins.scss b/src/wp-admin/css/colors/_mixins.scss index d33cf3bb2d854..d783bf268613e 100644 --- a/src/wp-admin/css/colors/_mixins.scss +++ b/src/wp-admin/css/colors/_mixins.scss @@ -1,39 +1,132 @@ @use 'sass:color'; +@use 'tokens'; /* - * Button mixin- creates a button effect with correct - * highlights/shadows, based on a base color. + * Button mixin - creates a primary button effect. + * Uses CSS custom properties for theme color support across color schemes. */ @mixin button( $button-color, $button-text-color: #fff ) { - background: $button-color; - border-color: $button-color; + background: var(--wp-admin-theme-color); + border-color: transparent; + border-radius: tokens.$radius-s; color: $button-text-color; - &:hover, - &:focus { - background: color.adjust($button-color, $lightness: 3%); - border-color: color.adjust($button-color, $lightness: -3%); + &:hover { + background: var(--wp-admin-theme-color-darker-10); + border-color: transparent; color: $button-text-color; } &:focus { + background: var(--wp-admin-theme-color); + border-color: transparent; + color: $button-text-color; + /* Gutenberg-style focus ring: outer theme color + inset white for contrast */ box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px $button-color; + 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), + inset 0 0 0 1px tokens.$white; + /* Visible in Windows High Contrast mode */ + outline: 1px solid transparent; } &:active { - background: color.adjust($button-color, $lightness: -5%); - border-color: color.adjust($button-color, $lightness: -5%); + background: var(--wp-admin-theme-color-darker-20); + border-color: transparent; color: $button-text-color; } + &:disabled, + &.disabled { + background: tokens.$gray-100; + border-color: transparent; + color: tokens.$gray-600; + cursor: not-allowed; + } + &.active, &.active:focus, &.active:hover { - background: $button-color; + background: var(--wp-admin-theme-color-darker-10); color: $button-text-color; - border-color: color.adjust($button-color, $lightness: -15%); - box-shadow: inset 0 2px 5px -3px color.adjust($button-color, $lightness: -50%); + border-color: transparent; + box-shadow: none; + } +} + +/* + * Secondary button mixin - outlined style with theme color. + * Matches Gutenberg's .is-secondary button variant. + */ +@mixin button-secondary() { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); + + &:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-300; + color: tokens.$gray-600; + cursor: not-allowed; + } +} + +/* + * Tertiary button mixin - transparent background, gray text. + */ +@mixin button-tertiary() { + background: transparent; + border: 1px solid tokens.$gray-600; + border-radius: tokens.$radius-s; + color: tokens.$gray-900; + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: tokens.$gray-900; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(0, 0, 0, 0.1); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-400; + color: tokens.$gray-600; + cursor: not-allowed; } } diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index e062a471d7150..393787f8e6c2a 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -631,11 +631,12 @@ code { top: -3px; margin-left: 4px; border: 1px solid #2271b1; - border-radius: 3px; - background: #f6f7f7; + border-radius: 2px; + background: transparent; font-size: 13px; - font-weight: 400; - line-height: 2.15384615; + font-weight: 500; + min-height: 40px; + line-height: 2.92307692; /* 38px for 40px height */ color: #2271b1; /* use the standard color used for buttons */ padding: 0 10px; min-height: 30px; @@ -649,7 +650,6 @@ code { .wrap .add-new-h2:hover, /* deprecated */ .wrap .page-title-action:hover { - background: #f0f0f1; border-color: #0a4b78; color: #0a4b78; } diff --git a/src/wp-includes/css/buttons.css b/src/wp-includes/css/buttons.css index 5146be4274254..3eda51ae5d788 100644 --- a/src/wp-includes/css/buttons.css +++ b/src/wp-includes/css/buttons.css @@ -46,15 +46,16 @@ TABLE OF CONTENTS: display: inline-block; text-decoration: none; font-size: 13px; - line-height: 2.15384615; /* 28px */ - min-height: 30px; + font-weight: 500; + line-height: 2.92307692; /* 38px - allows 40px min-height with 1px border */ + min-height: 40px; margin: 0; - padding: 0 10px; + padding: 0 16px; cursor: pointer; border-width: 1px; border-style: solid; -webkit-appearance: none; - border-radius: 3px; + border-radius: 2px; white-space: nowrap; box-sizing: border-box; } @@ -69,26 +70,36 @@ TABLE OF CONTENTS: padding: 0; } -.wp-core-ui .button.button-large, -.wp-core-ui .button-group.button-large .button { +/* Compact size - 32px, for space-constrained contexts */ +.wp-core-ui .button.button-compact, +.wp-core-ui .button-group.button-compact .button { + line-height: 2.30769231; /* 30px - allows 32px min-height with 1px border */ min-height: 32px; - line-height: 2.30769231; /* 30px */ padding: 0 12px; } +/* Small size - 24px */ .wp-core-ui .button.button-small, .wp-core-ui .button-group.button-small .button { - min-height: 26px; - line-height: 2.18181818; /* 24px */ + line-height: 2; /* 22px - allows 24px min-height with 1px border */ + min-height: 24px; padding: 0 8px; font-size: 11px; } +/* Large size - explicit 40px (same as default, for semantic clarity) */ +.wp-core-ui .button.button-large, +.wp-core-ui .button-group.button-large .button { + line-height: 2.92307692; /* 38px - allows 40px min-height with 1px border */ + min-height: 40px; + padding: 0 16px; +} + .wp-core-ui .button.button-hero, .wp-core-ui .button-group.button-hero .button { font-size: 14px; - min-height: 46px; - line-height: 3.14285714; + line-height: 3.28571429; /* 46px - allows 48px min-height with 1px border */ + min-height: 48px; padding: 0 36px; } @@ -115,9 +126,9 @@ TABLE OF CONTENTS: .wp-core-ui .button, .wp-core-ui .button-secondary { - color: #2271b1; - border-color: #2271b1; - background: #f6f7f7; + color: #3858e9; + border-color: #3858e9; + background: transparent; vertical-align: top; } @@ -127,21 +138,21 @@ TABLE OF CONTENTS: .wp-core-ui .button.hover, .wp-core-ui .button:hover, -.wp-core-ui .button-secondary:hover{ - background: #f0f0f1; - border-color: #0a4b78; - color: #0a4b78; +.wp-core-ui .button-secondary:hover { + background: rgba(56, 88, 233, 0.04); + border-color: #183ad6; + color: #183ad6; } .wp-core-ui .button.focus, .wp-core-ui .button:focus, .wp-core-ui .button-secondary:focus { - background: #f6f7f7; - border-color: #3582c4; - color: #0a4b78; - box-shadow: 0 0 0 1px #3582c4; + background: transparent; + border-color: #3858e9; + color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; /* Only visible in Windows High Contrast mode */ - outline: 2px solid transparent; + outline: 1px solid transparent; /* Reset inherited offset from Gutenberg */ outline-offset: 0; } @@ -149,25 +160,24 @@ TABLE OF CONTENTS: /* :active state */ .wp-core-ui .button:active, .wp-core-ui .button-secondary:active { - background: #f6f7f7; - border-color: #8c8f94; + background: rgba(56, 88, 233, 0.08); + border-color: #183ad6; + color: #183ad6; box-shadow: none; } /* pressed state e.g. a selected setting */ .wp-core-ui .button.active, .wp-core-ui .button.active:hover { - background-color: #dcdcde; - color: #135e96; - border-color: #0a4b78; - box-shadow: inset 0 2px 5px -3px #0a4b78; + background-color: rgba(56, 88, 233, 0.04); + color: #3858e9; + border-color: #3858e9; + box-shadow: none; } .wp-core-ui .button.active:focus { - border-color: #3582c4; - box-shadow: - inset 0 2px 5px -3px #0a4b78, - 0 0 0 1px #3582c4; + border-color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; } .wp-core-ui .button[disabled], @@ -177,9 +187,9 @@ TABLE OF CONTENTS: .wp-core-ui .button-secondary:disabled, .wp-core-ui .button-secondary.disabled, .wp-core-ui .button-disabled { - color: #a7aaad !important; - border-color: #dcdcde !important; - background: #f6f7f7 !important; + color: #949494 !important; + border-color: #dddddd !important; + background: transparent !important; box-shadow: none !important; cursor: default; transform: none !important; @@ -201,7 +211,7 @@ TABLE OF CONTENTS: cursor: pointer; text-align: left; /* Mimics the default link style in common.css */ - color: #2271b1; + color: #3858e9; text-decoration: underline; transition-property: border, background, color; transition-duration: .05s; @@ -210,14 +220,15 @@ TABLE OF CONTENTS: .wp-core-ui .button-link:hover, .wp-core-ui .button-link:active { - color: #135e96; + color: #183ad6; } .wp-core-ui .button-link:focus { - color: #043959; - box-shadow: 0 0 0 2px #2271b1; + color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; + border-radius: 2px; /* Only visible in Windows High Contrast mode */ - outline: 2px solid transparent; + outline: 1px solid transparent; } .wp-core-ui .button-link-delete { @@ -241,35 +252,37 @@ TABLE OF CONTENTS: ---------------------------------------------------------------------------- */ .wp-core-ui .button-primary { - background: #2271b1; - border-color: #2271b1; + background: #3858e9; + border-color: #3858e9; color: #fff; text-decoration: none; text-shadow: none; } .wp-core-ui .button-primary.hover, -.wp-core-ui .button-primary:hover, -.wp-core-ui .button-primary.focus, -.wp-core-ui .button-primary:focus { - background: #135e96; - border-color: #135e96; +.wp-core-ui .button-primary:hover { + background: #2145e6; + border-color: #2145e6; color: #fff; } .wp-core-ui .button-primary.focus, .wp-core-ui .button-primary:focus { + background: #3858e9; + border-color: #3858e9; + color: #fff; box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px #2271b1; + 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9, + inset 0 0 0 1px #fff; + outline: 1px solid transparent; } .wp-core-ui .button-primary.active, .wp-core-ui .button-primary.active:hover, .wp-core-ui .button-primary.active:focus, .wp-core-ui .button-primary:active { - background: #135e96; - border-color: #135e96; + background: #183ad6; + border-color: #183ad6; box-shadow: none; color: #fff; } @@ -278,9 +291,9 @@ TABLE OF CONTENTS: .wp-core-ui .button-primary:disabled, .wp-core-ui .button-primary-disabled, .wp-core-ui .button-primary.disabled { - color: #a7aaad !important; - background: #f6f7f7 !important; - border-color: #dcdcde !important; + color: #949494 !important; + background: #f0f0f0 !important; + border-color: #f0f0f0 !important; box-shadow: none !important; text-shadow: none !important; cursor: default; @@ -309,11 +322,11 @@ TABLE OF CONTENTS: } .wp-core-ui .button-group > .button:first-child { - border-radius: 3px 0 0 3px; + border-radius: 2px 0 0 2px; } .wp-core-ui .button-group > .button:last-child { - border-radius: 0 3px 3px 0; + border-radius: 0 2px 2px 0; } .wp-core-ui .button-group > .button-primary + .button { @@ -353,7 +366,7 @@ TABLE OF CONTENTS: input#save-post, a.preview { padding: 0 14px; - line-height: 2.71428571; /* 38px */ + line-height: 2.71428571; /* 38px - allows 40px min-height with 1px border */ font-size: 14px; vertical-align: middle; min-height: 40px; @@ -366,9 +379,9 @@ TABLE OF CONTENTS: } #media-upload.wp-core-ui .button { - padding: 0 10px 1px; + padding: 0 10px; + line-height: 1.69230769; /* 22px */ min-height: 24px; - line-height: 22px; font-size: 13px; } @@ -386,8 +399,8 @@ TABLE OF CONTENTS: .wp-core-ui.wp-customizer .button { font-size: 13px; - line-height: 2.15384615; /* 28px */ - min-height: 30px; + line-height: 2.30769231; /* 30px */ + min-height: 32px; margin: 0; vertical-align: inherit; } @@ -404,9 +417,9 @@ TABLE OF CONTENTS: /* Reset responsive styles on Log in button on iframed login form */ .interim-login .button.button-large { - min-height: 30px; - line-height: 2; - padding: 0 12px 2px; + min-height: 32px; + line-height: 2.30769231; /* 30px */ + padding: 0 12px; } } From f66c7b69e2338bcc57d6374a2876de292663fc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 12 Dec 2025 16:18:45 +0100 Subject: [PATCH 22/22] Admin UI: Fix theme card button sizing and visibility Update theme card buttons to work with the new design system sizing. **Button sizing:** - Use compact size (32px) for theme card buttons since they're in a space-constrained context - Set explicit min-height, line-height, and padding to match compact spec **Button visibility:** - Add white background to secondary buttons for visibility against the semi-transparent theme card overlay - Use :not(.button-primary) selector to preserve primary button styling - Adjust hover state to use #f0f0f0 background **Layout adjustments:** - Increase theme name vertical padding from 15px to 16px to accommodate taller buttons - Adjust active theme padding-right from 110px to 115px for button width - Reduce theme-actions horizontal padding from 15px to 12px See: https://core.trac.wordpress.org/ticket/64308 --- src/wp-admin/css/themes.css | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index ea62a09cf1ed1..113cec87f50e7 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -83,7 +83,7 @@ body.js .theme-browser.search-loading { font-weight: 600; height: 18px; margin: 0; - padding: 15px; + padding: 16px 15px; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); overflow: hidden; white-space: nowrap; @@ -112,9 +112,26 @@ body.js .theme-browser.search-loading { margin-right: 3px; } +/* Use compact size for space-constrained theme cards */ .theme-browser .theme .theme-actions .button { float: none; margin-left: 3px; + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px min-height */ + padding: 0 12px; +} + +/* Secondary buttons need white background for visibility on semi-transparent overlay */ +.theme-browser .theme .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):focus { + background: #fff; } /** @@ -211,7 +228,7 @@ body.js .theme-browser.search-loading { .theme-browser .theme.active .theme-name { background: #1d2327; color: #fff; - padding-right: 110px; + padding-right: 115px; font-weight: 300; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.5); } @@ -240,7 +257,7 @@ body.js .theme-browser.search-loading { top: 50%; transform: translateY(-50%); right: 0; - padding: 9px 15px; + padding: 9px 12px; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); } @@ -248,6 +265,19 @@ body.js .theme-browser.search-loading { margin-right: 0; } +/* Active theme secondary buttons need white background for visibility on dark overlay */ +.theme-browser .theme.active .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):focus { + background: #fff; +} + .theme-browser .theme .theme-author { background: #1d2327; color: #f0f0f1;