Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ac28493
Global Styles: Add Forms section
MaggieCabrera Aug 14, 2026
2787163
Global Styles: Add Elements section
MaggieCabrera Aug 14, 2026
b362992
Global Styles: Give elements their own home and the full control set
MaggieCabrera Sep 4, 2026
e5ccb54
Global Styles: Preview elements with the site's own styles
MaggieCabrera Sep 4, 2026
568979a
Global Styles: List elements in one flat list with blocks
MaggieCabrera Sep 15, 2026
f1a3a1b
Global Styles: Resolve an element's styles once per screen
MaggieCabrera Sep 15, 2026
f895665
Global Styles: Share border handling and the CSS capability check
MaggieCabrera Sep 15, 2026
e42c081
Global Styles: Restore the Text and All headings entries and fix CI
MaggieCabrera Sep 16, 2026
4b31bf3
Global Styles: Reformat for the wp-prettier upgrade on trunk
MaggieCabrera Sep 29, 2026
c8632a5
Icons: Move the formInput changelog entry to Unreleased
MaggieCabrera Sep 29, 2026
aac3306
Global Styles: Mention elements in the Blocks & Elements description
MaggieCabrera Sep 29, 2026
2a92d72
Blocks: Keep text alignment for elements in Global Styles
MaggieCabrera Sep 29, 2026
72cd2f8
Global Styles: Offer states on the link and button element screens
MaggieCabrera Sep 29, 2026
984ba05
Global Styles: Show the default background size on element screens
MaggieCabrera Sep 29, 2026
a330047
Global Styles: Use one route for the element screens
MaggieCabrera Sep 29, 2026
c129bee
Global Styles: Share element titles between the list and their screens
MaggieCabrera Sep 29, 2026
c706df8
Global Styles: Restore trunk's formatting of a media query
MaggieCabrera Sep 30, 2026
5410a24
Global Styles: Drop the deprecated spacer from the element screen
MaggieCabrera Sep 30, 2026
a19876a
Global Styles: Keep trunk's Text row under Typography
MaggieCabrera Oct 14, 2026
4703e54
Move this PR's changelog entries back under Unreleased
MaggieCabrera Oct 14, 2026
1680f55
Global Styles: Drop the custom styles dot from element rows
MaggieCabrera Oct 14, 2026
f5c7771
Global Styles: Keep the border helper in the block screen
MaggieCabrera Oct 14, 2026
57d9d27
Global Styles: Inline the customized check for elements
MaggieCabrera Oct 14, 2026
e3cca76
Global Styles: Check that searching lists matching elements
MaggieCabrera Oct 14, 2026
5ba39ae
Style Book: Show the Text category on element screens
MaggieCabrera Oct 14, 2026
cdb44e0
Global Styles: Match element names and group titles in search
MaggieCabrera Oct 14, 2026
f2115f4
Fix the formatting of the heading styles end-to-end test
MaggieCabrera Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions packages/blocks/src/store/private-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
import deprecated from '@wordpress/deprecated';
import { getBlockType } from './selectors';
import { getValueFromObjectPath } from './utils';
import { __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY } from '../api/constants';
import {
__EXPERIMENTAL_ELEMENTS as ELEMENTS,
__EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY,
} from '../api/constants';
import type { BlockStoreState } from './types';
import type {
BlockAttribute,
Expand Down Expand Up @@ -56,6 +59,30 @@ const ROOT_BLOCK_SUPPORTS: string[] = [
'letterSpacing',
];

/*
* theme.json accepts the complete style set on every element, and the styles
* engine emits all of it, so Global Styles offers elements the same controls it
* offers blocks. These are the supports missing from the root list above.
*
* `filter` is deliberately absent: theme.json validates a duotone on an element
* but the engine emits no CSS for it, so the control would do nothing. Sizing
* is left out entirely (aspect ratio, height, width and their minimums)
* because an element is sized by the block that contains it.
*/
const ELEMENT_BLOCK_SUPPORTS: string[] = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ELEMENT_BLOCK_SUPPORTS starts from ROOT_BLOCK_SUPPORTS, elements keep contentSize and wideSize. The width controls never render because ScreenElement doesn't pass includeLayoutControls, but useHasDimensionsPanel still counts them. So on a theme that sets layout widths without enabling padding or margin (no appearanceTools), every element screen shows an empty "Dimensions" panel. Filtering contentSize and wideSize here alongside blockGap would fix it. The expected lists in test/private-selectors.js would need updating too.

...ROOT_BLOCK_SUPPORTS,
'backgroundGradient',
'backgroundImage',
'backgroundSize',
'borderColor',
'borderRadius',
'borderStyle',
'borderWidth',
'margin',
'shadow',
'writingMode',
];

/**
* Filters the list of supported styles for a given element.
*
Expand Down Expand Up @@ -122,6 +149,14 @@ function filterElementBlockSupports(
return false;
}

const isElement = ! name && !! element && element in ELEMENTS;

// Block gap is emitted for blocks and for the root, but not for an
// element's own selector, so the control would have no effect there.
if ( support === 'blockGap' && isElement ) {
return false;
}

return true;
} );
}
Expand All @@ -136,8 +171,11 @@ export const getSupportedStyles = createSelector(
element: string | undefined
): string[] => {
if ( ! name ) {
// `element` also carries UI-only names such as 'text', which mean
// the root rather than an element, and keep the root supports.
const isElement = !! element && element in ELEMENTS;
return filterElementBlockSupports(
ROOT_BLOCK_SUPPORTS,
isElement ? ELEMENT_BLOCK_SUPPORTS : ROOT_BLOCK_SUPPORTS,
name,
element
);
Expand Down
22 changes: 20 additions & 2 deletions packages/blocks/src/store/test/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,19 @@ describe( 'private selectors', () => {
'padding',
'contentSize',
'wideSize',
'blockGap',
'textAlign',
'textDecoration',
'textShadow',
'backgroundGradient',
'backgroundImage',
'backgroundSize',
'borderColor',
'borderRadius',
'borderStyle',
'borderWidth',
'margin',
'shadow',
'writingMode',
] );
} );

Expand All @@ -92,11 +101,20 @@ describe( 'private selectors', () => {
'padding',
'contentSize',
'wideSize',
'blockGap',
'textAlign',
'textTransform',
'textShadow',
'letterSpacing',
'backgroundGradient',
'backgroundImage',
'backgroundSize',
'borderColor',
'borderRadius',
'borderStyle',
'borderWidth',
'margin',
'shadow',
'writingMode',
] );
} );

Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/components/global-styles/block-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export function GlobalStylesBlockLink( { path, onPathChange } ) {
}
if (
! path ||
( path !== '/blocks' && ! path.startsWith( '/blocks/' ) )
( path !== '/blocks' && ! path.startsWith( '/blocks/' ) ) ||
path.startsWith( '/blocks/elements' )
) {
return;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/editor/src/components/style-book/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,12 @@ export const StyleBookPreview = ( {
let blockVariation = null;
if ( section.includes( '/colors' ) ) {
previewCategory = 'colors';
} else if ( section.includes( '/typography' ) ) {
} else if (
section.includes( '/typography' ) ||
// Element screens show the Text category, as they did when they
// lived under Typography.
section.startsWith( '/blocks/elements/' )
) {
previewCategory = 'text';
} else if ( section.includes( '/blocks' ) ) {
previewCategory = 'blocks';
Expand Down
3 changes: 3 additions & 0 deletions packages/global-styles-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

### Enhancements

- List elements alongside blocks in one Blocks & Elements screen, grouped under Elements, Headings and Form controls, instead of splitting them between Typography and Colors ([#81645](https://github.com/WordPress/gutenberg/pull/81645)).
- Offer every element the background, dimensions, border, shadow and custom CSS controls that `theme.json` already accepts, instead of typography and color alone ([#81645](https://github.com/WordPress/gutenberg/pull/81645)).
- Preview an element as a sample of the markup it applies to, rendered with the site's own styles, so the preview reflects every panel rather than typography and color alone ([#81645](https://github.com/WordPress/gutenberg/pull/81645)).
- Remove the dot that marks blocks with user styles in the block list. The filter to show only those blocks stays ([#84657](https://github.com/WordPress/gutenberg/pull/84657)).
- Font Library: Mark installed font variants in font collections ([#84023](https://github.com/WordPress/gutenberg/pull/84023)).

Expand Down
156 changes: 156 additions & 0 deletions packages/global-styles-ui/src/element-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { getBlockType } from '@wordpress/blocks';
import {
__unstableIframe as Iframe,
__unstableEditorStyles as EditorStyles,
store as blockEditorStore,
// @ts-expect-error: Not typed yet.
} from '@wordpress/block-editor';

interface ElementPreviewProps {
element: string;
}

/*
* Centres the sample and strips the canvas chrome. Everything the sample
* actually looks like comes from the site's own stylesheet.
*/
const PREVIEW_CSS = `
body {
margin: 0;
padding: 16px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
overflow: hidden;
}
figure.wp-block-image img.global-styles-ui-element-preview__media {
display: block;
width: 100%;
height: 44px;
max-height: 44px;
border-radius: 2px;
object-fit: cover;
aspect-ratio: auto;
}
figure {
margin: 0;
max-width: 160px;
}
blockquote {
margin: 0;
max-width: 220px;
}
blockquote p {
margin: 0 0 4px;
}
`;

/**
* Renders a sample of an element using the site's own styles.
*
* The sample is real markup carrying the classes the element's selector
* targets, rendered in the editor's iframe with the generated stylesheet, so it
* picks up every panel, the theme's fonts and the cascade, rather than a list of
* properties copied across by hand.
*
* @param props
* @param props.element The element being previewed.
*/
export default function ElementPreview( { element }: ElementPreviewProps ) {
const styles = useSelect(
( select ) => select( blockEditorStore ).getSettings().styles,
[]
);

const editorStyles = useMemo(
() => [ ...( styles ?? [] ), { css: PREVIEW_CSS } ],
[ styles ]
);

// Show a caption on the image the Image block uses for its own example,
// rather than keeping a second copy of that URL here.
const imageUrl = getBlockType( 'core/image' )?.example?.attributes?.url as
string | undefined;

let sample;
switch ( element ) {
case 'button':
// The class is half of the element's selector, so a button only
// picks up the element's styles when it carries it.
sample = (
<button className="wp-element-button" type="button">
{ __( 'Call to action' ) }
</button>
);
break;
case 'textInput':
sample = (
<input type="text" defaultValue={ __( 'Text' ) } readOnly />
);
break;
case 'select':
sample = (
<select>
<option>{ __( 'Select an option' ) }</option>
</select>
);
break;
case 'caption':
sample = (
<figure className="wp-block-image">
{ imageUrl && (
<img
className="global-styles-ui-element-preview__media"
src={ imageUrl }
alt=""
/>
) }
<figcaption className="wp-element-caption">
{ __( 'A caption for the image above' ) }
</figcaption>
</figure>
);
break;
case 'cite':
sample = (
<blockquote>
<p>{ __( 'In quoting others, we cite ourselves.' ) }</p>
<cite>{ __( 'Julio Cortázar' ) }</cite>
</blockquote>
);
break;
case 'link':
sample = <a href="#anchor">{ __( 'A link' ) }</a>;
break;
case 'heading':
// The shared heading entry has no tag of its own; any level shows
// what it sets, and h2 is the most common in content.
sample = <h2>{ __( 'Aa' ) }</h2>;
break;
default: {
// The heading level elements are named after their tag.
const Tag = ( /^h[1-6]$/.test( element ) ? element : 'p' ) as
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
sample = <Tag>{ __( 'Aa' ) }</Tag>;
}
}

return (
<div className="global-styles-ui-element-preview">
<Iframe
tabIndex={ -1 }
readonly
title={ __( 'Element preview' ) }
aria-hidden="true"
>
<EditorStyles styles={ editorStyles } />
{ sample }
</Iframe>
</div>
);
}
34 changes: 9 additions & 25 deletions packages/global-styles-ui/src/global-styles-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ScreenRoot from './screen-root';
import ScreenBlockList from './screen-block-list';
import ScreenBlock from './screen-block';
import ScreenTypography from './screen-typography';
import ScreenTypographyElement from './screen-typography-element';
import ScreenElement from './screen-element';
import ScreenColors from './screen-colors';
import ScreenColorPalette from './screen-color-palette';
import ScreenBackground from './screen-background';
Expand Down Expand Up @@ -206,6 +206,9 @@ export function GlobalStylesUI( {
<GlobalStylesNavigationScreen path="/typography">
<ScreenTypography />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/text">
<ScreenElement element="text" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/font-sizes">
<FontSizes />
</GlobalStylesNavigationScreen>
Expand Down Expand Up @@ -242,33 +245,14 @@ export function GlobalStylesUI( {
<GlobalStylesNavigationScreen path="/background">
<ScreenBackground />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/text">
<ScreenTypographyElement element="text" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/link">
<ScreenTypographyElement element="link" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/heading">
<ScreenTypographyElement element="heading" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/caption">
<ScreenTypographyElement element="caption" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/cite">
<ScreenTypographyElement element="cite" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/button">
<ScreenTypographyElement element="button" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/textInput">
<ScreenTypographyElement element="textInput" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/typography/select">
<ScreenTypographyElement element="select" />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/blocks">
<ScreenBlockList />
</GlobalStylesNavigationScreen>
<GlobalStylesNavigationScreen path="/blocks/elements/:element">
<ScreenElement
showStateControls={ showBlockStateControls }
/>
</GlobalStylesNavigationScreen>
{ blocks.map( ( block ) => (
<Fragment key={ block.name }>
<GlobalStylesNavigationScreen
Expand Down
22 changes: 22 additions & 0 deletions packages/global-styles-ui/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,25 @@ export function useStyleWithResolvedBackground(
return { ...style, background };
}, [ style, merged ] );
}

/**
* Whether the current user may edit custom CSS in Global Styles.
*
* The capability is reported as a link on the global styles record rather
* than as a user capability.
*/
export function useCanEditCSS(): boolean {
return useSelect( ( select ) => {
const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } =
select( coreStore );

const globalStylesId = __experimentalGetCurrentGlobalStylesId();
const globalStyles = globalStylesId
? getEntityRecord( 'root', 'globalStyles', globalStylesId )
: undefined;

return !! ( globalStyles as GlobalStylesConfig )?._links?.[
'wp:action-edit-css'
];
}, [] );
}
Loading
Loading