diff --git a/readme.txt b/readme.txt
index 952baad6..7c5a8eaf 100644
--- a/readme.txt
+++ b/readme.txt
@@ -20,6 +20,7 @@ This plugin allows you to:
- Create a new style variation
- Export a theme
- Save user changed templates and styles to the active theme
+- Expand the typography controls in the block inspector while authoring a theme (opt-in, under Editor preferences)
All newly created themes or style variations will include changes made within the WordPress Editor.
diff --git a/src/editor-enhancements/expand-typography-controls.js b/src/editor-enhancements/expand-typography-controls.js
new file mode 100644
index 00000000..f7001fa1
--- /dev/null
+++ b/src/editor-enhancements/expand-typography-controls.js
@@ -0,0 +1,72 @@
+/**
+ * WordPress dependencies
+ */
+// eslint-disable-next-line import/no-unresolved -- Provided as an external at build time via @wordpress/dependency-extraction-webpack-plugin.
+import { addFilter } from '@wordpress/hooks';
+import { select } from '@wordpress/data';
+import { store as preferencesStore } from '@wordpress/preferences';
+
+// Mapping verified against
+// gutenberg/packages/block-editor/src/components/global-styles/typography-panel.js
+// fontWeight and fontStyle collapse into the single "fontAppearance" control.
+export const TYPOGRAPHY_SUPPORT_TO_CONTROL = {
+ fontSize: 'fontSize',
+ lineHeight: 'lineHeight',
+ textAlign: 'textAlign',
+ textColumns: 'textColumns',
+ textIndent: 'textIndent',
+ __experimentalFontFamily: 'fontFamily',
+ __experimentalLetterSpacing: 'letterSpacing',
+ __experimentalTextDecoration: 'textDecoration',
+ __experimentalTextTransform: 'textTransform',
+ __experimentalWritingMode: 'writingMode',
+ __experimentalFontWeight: 'fontAppearance',
+ __experimentalFontStyle: 'fontAppearance',
+};
+
+export const PREFERENCE_SCOPE = 'create-block-theme';
+export const PREFERENCE_KEY = 'expandAllTypographyControls';
+
+export function expandTypographyDefaults( typographySupport ) {
+ if ( ! typographySupport || typeof typographySupport !== 'object' ) {
+ return typographySupport;
+ }
+ const defaults = {};
+ for ( const [ supportKey, controlKey ] of Object.entries(
+ TYPOGRAPHY_SUPPORT_TO_CONTROL
+ ) ) {
+ if ( typographySupport[ supportKey ] ) {
+ defaults[ controlKey ] = true;
+ }
+ }
+ return {
+ ...typographySupport,
+ __experimentalDefaultControls: {
+ ...( typographySupport.__experimentalDefaultControls ?? {} ),
+ ...defaults,
+ },
+ };
+}
+
+addFilter(
+ 'blocks.registerBlockType',
+ 'create-block-theme/expand-typography-controls',
+ ( settings ) => {
+ const enabled = select( preferencesStore )?.get?.(
+ PREFERENCE_SCOPE,
+ PREFERENCE_KEY
+ );
+ if ( ! enabled || ! settings?.supports?.typography ) {
+ return settings;
+ }
+ return {
+ ...settings,
+ supports: {
+ ...settings.supports,
+ typography: expandTypographyDefaults(
+ settings.supports.typography
+ ),
+ },
+ };
+ }
+);
diff --git a/src/editor-sidebar/editor-preferences-panel.js b/src/editor-sidebar/editor-preferences-panel.js
new file mode 100644
index 00000000..6273b39c
--- /dev/null
+++ b/src/editor-sidebar/editor-preferences-panel.js
@@ -0,0 +1,72 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { store as preferencesStore } from '@wordpress/preferences';
+import {
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalVStack as VStack,
+ Card,
+ CardBody,
+ CheckboxControl,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import ScreenHeader from './screen-header';
+import {
+ PREFERENCE_SCOPE,
+ PREFERENCE_KEY,
+} from '../editor-enhancements/expand-typography-controls';
+
+export const EditorPreferencesPanel = () => {
+ const expandAllTypographyControls = useSelect(
+ ( select ) =>
+ !! select( preferencesStore ).get(
+ PREFERENCE_SCOPE,
+ PREFERENCE_KEY
+ ),
+ []
+ );
+
+ const { set: setPreference } = useDispatch( preferencesStore );
+
+ const handleToggle = ( value ) => {
+ setPreference( PREFERENCE_SCOPE, PREFERENCE_KEY, value );
+ // eslint-disable-next-line no-alert
+ window.alert(
+ __(
+ 'Preference updated. The editor will now reload.',
+ 'create-block-theme'
+ )
+ );
+ window.location.reload();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/plugin-sidebar.js b/src/plugin-sidebar.js
index 90d487d0..2d283361 100644
--- a/src/plugin-sidebar.js
+++ b/src/plugin-sidebar.js
@@ -1,3 +1,10 @@
+/**
+ * Internal dependencies
+ */
+// Side-effect import — must load before block registration so the
+// registerBlockType filter is in place when blocks register.
+import './editor-enhancements/expand-typography-controls';
+
/**
* WordPress dependencies
*/
@@ -39,6 +46,7 @@ import {
blockMeta,
help,
trash,
+ cog,
} from '@wordpress/icons';
/**
@@ -55,6 +63,7 @@ import { downloadExportedTheme } from './resolvers';
import downloadFile from './utils/download-file';
import AboutPlugin from './editor-sidebar/about';
import ResetTheme from './editor-sidebar/reset-theme';
+import { EditorPreferencesPanel } from './editor-sidebar/editor-preferences-panel';
import './plugin-styles.scss';
function PluginSidebarItem( { icon, path, children, onClick } ) {
@@ -229,6 +238,20 @@ const CreateBlockThemePlugin = () => {
+
+
+
+ { __(
+ 'Editor preferences',
+ 'create-block-theme'
+ ) }
+
+
+
+
{
+
+
+
+
diff --git a/src/test/expand-typography-controls.test.js b/src/test/expand-typography-controls.test.js
new file mode 100644
index 00000000..f5164413
--- /dev/null
+++ b/src/test/expand-typography-controls.test.js
@@ -0,0 +1,173 @@
+// The module under test imports WordPress packages that are provided as
+// externals at build time and are not installed as npm dependencies. Mock them
+// so Jest can resolve the module to test the pure mapping function.
+jest.mock( '@wordpress/hooks', () => ( { addFilter: jest.fn() } ), {
+ virtual: true,
+} );
+jest.mock( '@wordpress/data', () => ( { select: jest.fn() } ), {
+ virtual: true,
+} );
+jest.mock( '@wordpress/preferences', () => ( { store: 'core/preferences' } ), {
+ virtual: true,
+} );
+
+/**
+ * Internal dependencies
+ */
+// eslint-disable-next-line import/first
+import {
+ expandTypographyDefaults,
+ TYPOGRAPHY_SUPPORT_TO_CONTROL,
+} from '../editor-enhancements/expand-typography-controls';
+
+describe( 'expandTypographyDefaults', () => {
+ it( 'returns input unchanged when null', () => {
+ expect( expandTypographyDefaults( null ) ).toBe( null );
+ } );
+
+ it( 'returns input unchanged when undefined', () => {
+ expect( expandTypographyDefaults( undefined ) ).toBe( undefined );
+ } );
+
+ it( 'returns input unchanged when not an object', () => {
+ expect( expandTypographyDefaults( true ) ).toBe( true );
+ } );
+
+ it( 'yields an empty defaults map when no supports are enabled', () => {
+ expect( expandTypographyDefaults( {} ) ).toEqual( {
+ __experimentalDefaultControls: {},
+ } );
+ } );
+
+ it( 'maps fontSize to fontSize', () => {
+ expect( expandTypographyDefaults( { fontSize: true } ) ).toEqual( {
+ fontSize: true,
+ __experimentalDefaultControls: { fontSize: true },
+ } );
+ } );
+
+ it( 'maps __experimentalFontFamily to fontFamily', () => {
+ expect(
+ expandTypographyDefaults( { __experimentalFontFamily: true } )
+ ).toEqual( {
+ __experimentalFontFamily: true,
+ __experimentalDefaultControls: { fontFamily: true },
+ } );
+ } );
+
+ it( 'maps __experimentalFontWeight alone to fontAppearance', () => {
+ expect(
+ expandTypographyDefaults( { __experimentalFontWeight: true } )
+ ).toEqual( {
+ __experimentalFontWeight: true,
+ __experimentalDefaultControls: { fontAppearance: true },
+ } );
+ } );
+
+ it( 'maps __experimentalFontStyle alone to fontAppearance', () => {
+ expect(
+ expandTypographyDefaults( { __experimentalFontStyle: true } )
+ ).toEqual( {
+ __experimentalFontStyle: true,
+ __experimentalDefaultControls: { fontAppearance: true },
+ } );
+ } );
+
+ it( 'collapses both weight and style into a single fontAppearance entry', () => {
+ const result = expandTypographyDefaults( {
+ __experimentalFontWeight: true,
+ __experimentalFontStyle: true,
+ } );
+ expect( result.__experimentalDefaultControls ).toEqual( {
+ fontAppearance: true,
+ } );
+ expect(
+ Object.keys( result.__experimentalDefaultControls ).length
+ ).toBe( 1 );
+ } );
+
+ it( 'ignores support keys that are falsy', () => {
+ expect(
+ expandTypographyDefaults( {
+ fontSize: true,
+ lineHeight: false,
+ textAlign: undefined,
+ } )
+ ).toEqual( {
+ fontSize: true,
+ lineHeight: false,
+ textAlign: undefined,
+ __experimentalDefaultControls: { fontSize: true },
+ } );
+ } );
+
+ it( 'maps a full realistic supports.typography shape to every expected control', () => {
+ const fullShape = {
+ fontSize: true,
+ lineHeight: true,
+ textAlign: true,
+ textColumns: true,
+ textIndent: true,
+ __experimentalFontFamily: true,
+ __experimentalLetterSpacing: true,
+ __experimentalTextDecoration: true,
+ __experimentalTextTransform: true,
+ __experimentalWritingMode: true,
+ __experimentalFontWeight: true,
+ __experimentalFontStyle: true,
+ };
+ expect(
+ expandTypographyDefaults( fullShape ).__experimentalDefaultControls
+ ).toEqual( {
+ fontSize: true,
+ lineHeight: true,
+ textAlign: true,
+ textColumns: true,
+ textIndent: true,
+ fontFamily: true,
+ letterSpacing: true,
+ textDecoration: true,
+ textTransform: true,
+ writingMode: true,
+ fontAppearance: true,
+ } );
+ } );
+
+ it( 'preserves unknown keys already set in __experimentalDefaultControls', () => {
+ const input = {
+ fontSize: true,
+ __experimentalDefaultControls: {
+ fontSize: false,
+ someFutureControl: true,
+ },
+ };
+ const result = expandTypographyDefaults( input );
+ expect( result.__experimentalDefaultControls ).toEqual( {
+ fontSize: true,
+ someFutureControl: true,
+ } );
+ } );
+
+ it( 'preserves other keys on the typography supports object', () => {
+ const input = {
+ fontSize: true,
+ customKey: 'untouched',
+ };
+ const result = expandTypographyDefaults( input );
+ expect( result.customKey ).toBe( 'untouched' );
+ expect( result.__experimentalDefaultControls ).toEqual( {
+ fontSize: true,
+ } );
+ } );
+} );
+
+describe( 'TYPOGRAPHY_SUPPORT_TO_CONTROL', () => {
+ it( 'merges fontWeight and fontStyle into fontAppearance', () => {
+ expect( TYPOGRAPHY_SUPPORT_TO_CONTROL.__experimentalFontWeight ).toBe(
+ 'fontAppearance'
+ );
+ expect( TYPOGRAPHY_SUPPORT_TO_CONTROL.__experimentalFontStyle ).toBe(
+ 'fontAppearance'
+ );
+ } );
+} );