+
) }
@@ -208,7 +232,7 @@ function RevisionsButtons( {
{ isSelected &&
( areStylesEqual ? (
-
+
{ __(
'These styles are already applied to your site.'
) }
@@ -217,7 +241,7 @@ function RevisionsButtons( {
li,
- .edit-site-global-styles-screen-revisions__meta,
- .edit-site-global-styles-screen-revisions__applied-text {
+ .global-styles-ui-screen-revisions__changes > li,
+ .global-styles-ui-screen-revisions__meta,
+ .global-styles-ui-screen-revisions__applied-text {
color: $gray-900;
}
}
@@ -92,38 +92,38 @@
height: $grid-unit-20 + 2;
}
}
-.edit-site-global-styles-screen-revisions__revision-item-wrapper {
+.global-styles-ui-screen-revisions__revision-item-wrapper {
display: block;
padding: $grid-unit-15 $grid-unit-15 $grid-unit-05 $grid-unit-50;
}
-.edit-site-global-styles-screen-revisions__apply-button.is-primary,
-.edit-site-global-styles-screen-revisions__applied-text {
+.global-styles-ui-screen-revisions__apply-button.is-primary,
+.global-styles-ui-screen-revisions__applied-text {
align-self: flex-start;
- // Left margin = left padding of .edit-site-global-styles-screen-revisions__revision-item-wrapper.
+ // Left margin = left padding of .global-styles-ui-screen-revisions__revision-item-wrapper.
margin: $grid-unit-05 $grid-unit-15 $grid-unit-15 $grid-unit-50;
}
-.edit-site-global-styles-screen-revisions__changes,
-.edit-site-global-styles-screen-revisions__meta,
-.edit-site-global-styles-screen-revisions__applied-text {
+.global-styles-ui-screen-revisions__changes,
+.global-styles-ui-screen-revisions__meta,
+.global-styles-ui-screen-revisions__applied-text {
color: $gray-700;
font-size: 12px;
}
-.edit-site-global-styles-screen-revisions__description {
+.global-styles-ui-screen-revisions__description {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $grid-unit-10;
- .edit-site-global-styles-screen-revisions__date {
+ .global-styles-ui-screen-revisions__date {
text-transform: uppercase;
font-weight: 600;
font-size: 12px;
}
}
-.edit-site-global-styles-screen-revisions__meta {
+.global-styles-ui-screen-revisions__meta {
display: flex;
justify-content: start;
width: 100%;
@@ -139,11 +139,11 @@
}
}
-.edit-site-global-styles-screen-revisions__loading {
+.global-styles-ui-screen-revisions__loading {
margin: $grid-unit-30 auto !important;
}
-.edit-site-global-styles-screen-revisions__changes {
+.global-styles-ui-screen-revisions__changes {
text-align: left;
line-height: $default-line-height;
margin-left: $grid-unit-15;
@@ -154,7 +154,7 @@
}
}
-.edit-site-global-styles-screen-revisions__pagination.edit-site-global-styles-screen-revisions__pagination {
+.global-styles-ui-screen-revisions__pagination.global-styles-ui-screen-revisions__pagination {
justify-content: space-between;
gap: 2px;
.edit-site-pagination__total {
@@ -182,7 +182,7 @@
}
}
-.edit-site-global-styles-screen-revisions__footer {
+.global-styles-ui-screen-revisions__footer {
height: $grid-unit-70;
z-index: 1;
position: sticky;
diff --git a/packages/global-styles-ui/src/screen-revisions/types.ts b/packages/global-styles-ui/src/screen-revisions/types.ts
new file mode 100644
index 00000000000000..8b03337c11d898
--- /dev/null
+++ b/packages/global-styles-ui/src/screen-revisions/types.ts
@@ -0,0 +1,18 @@
+/**
+ * WordPress dependencies
+ */
+import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
+
+export interface User {
+ id: number;
+ name: string;
+ avatar_urls: Record< string, string >;
+}
+
+export interface Revision extends GlobalStylesConfig {
+ id: string | number;
+ author?: User;
+ modified?: string | Date;
+ isLatest?: boolean;
+ _links?: any;
+}
diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/global-styles-ui/src/screen-revisions/use-global-styles-revisions.tsx
similarity index 56%
rename from packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
rename to packages/global-styles-ui/src/screen-revisions/use-global-styles-revisions.tsx
index 024fb74ef97cf2..514ac0ba420f31 100644
--- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
+++ b/packages/global-styles-ui/src/screen-revisions/use-global-styles-revisions.tsx
@@ -2,14 +2,37 @@
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
-import { store as coreStore } from '@wordpress/core-data';
+import {
+ store as coreStore,
+ type GlobalStylesRevision,
+} from '@wordpress/core-data';
import { useContext, useMemo } from '@wordpress/element';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
-import { unlock } from '../../../lock-unlock';
+import { GlobalStylesContext } from '../context';
+import type { Revision, User } from './types';
+
+interface RawRevision extends Omit< Revision, 'author' > {
+ author?: number;
+}
+
+interface Query {
+ per_page?: number;
+ page?: number;
+}
+
+interface UseGlobalStylesRevisionsParams {
+ query?: Query;
+}
+
+interface UseGlobalStylesRevisionsReturn {
+ revisions: Revision[];
+ hasUnsavedChanges: boolean;
+ isLoading: boolean;
+ revisionsCount: number;
+}
const SITE_EDITOR_AUTHORS_QUERY = {
per_page: -1,
@@ -18,11 +41,16 @@ const SITE_EDITOR_AUTHORS_QUERY = {
capabilities: [ 'edit_theme_options' ],
};
const DEFAULT_QUERY = { per_page: 100, page: 1 };
-const EMPTY_ARRAY = [];
-const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
-export default function useGlobalStylesRevisions( { query } = {} ) {
+const EMPTY_ARRAY: RawRevision[] = [];
+
+export default function useGlobalStylesRevisions( {
+ query,
+}: UseGlobalStylesRevisionsParams = {} ): UseGlobalStylesRevisionsReturn {
const { user: userConfig } = useContext( GlobalStylesContext );
- const _query = { ...DEFAULT_QUERY, ...query };
+ const _query: Query = useMemo(
+ () => ( { ...DEFAULT_QUERY, ...query } ),
+ [ query ]
+ );
const {
authors,
currentUser,
@@ -39,32 +67,44 @@ export default function useGlobalStylesRevisions( { query } = {} ) {
getRevisions,
__experimentalGetCurrentGlobalStylesId,
getEntityRecord,
+ // @ts-expect-error
isResolving,
} = select( coreStore );
- const dirtyEntityRecords = __experimentalGetDirtyEntityRecords();
+ const dirtyEntityRecords =
+ __experimentalGetDirtyEntityRecords() || [];
const _currentUser = getCurrentUser();
const _isDirty = dirtyEntityRecords.length > 0;
const globalStylesId = __experimentalGetCurrentGlobalStylesId();
const globalStyles = globalStylesId
- ? getEntityRecord( 'root', 'globalStyles', globalStylesId )
+ ? getEntityRecord< GlobalStylesRevision >(
+ 'root',
+ 'globalStyles',
+ globalStylesId
+ )
: undefined;
- const _revisionsCount =
+ const _revisionsCount: number =
+ // @ts-expect-error - _links is not typed in GlobalStylesRevision
globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0;
- const globalStylesRevisions =
- getRevisions(
- 'root',
- 'globalStyles',
- globalStylesId,
- _query
- ) || EMPTY_ARRAY;
- const _authors =
- getUsers( SITE_EDITOR_AUTHORS_QUERY ) || EMPTY_ARRAY;
- const _isResolving = isResolving( 'getRevisions', [
- 'root',
- 'globalStyles',
- globalStylesId,
- _query,
- ] );
+ // @ts-expect-error - getRevisions is not fully typed
+ const globalStylesRevisions: RawRevision[] = globalStylesId
+ ? getRevisions(
+ 'root',
+ 'globalStyles',
+ globalStylesId,
+ _query
+ ) || EMPTY_ARRAY
+ : EMPTY_ARRAY;
+ // @ts-expect-error - getUsers is not fully typed
+ const _authors: User[] =
+ getUsers( SITE_EDITOR_AUTHORS_QUERY ) || [];
+ const _isResolving = globalStylesId
+ ? isResolving( 'getRevisions', [
+ 'root',
+ 'globalStyles',
+ globalStylesId,
+ _query,
+ ] )
+ : false;
return {
authors: _authors,
currentUser: _currentUser,
@@ -74,12 +114,12 @@ export default function useGlobalStylesRevisions( { query } = {} ) {
revisionsCount: _revisionsCount,
};
},
- [ query ]
+ [ _query ]
);
- return useMemo( () => {
+ return useMemo( (): UseGlobalStylesRevisionsReturn => {
if ( ! authors.length || isLoadingGlobalStylesRevisions ) {
return {
- revisions: EMPTY_ARRAY,
+ revisions: EMPTY_ARRAY as Revision[],
hasUnsavedChanges: isDirty,
isLoading: true,
revisionsCount,
@@ -87,7 +127,7 @@ export default function useGlobalStylesRevisions( { query } = {} ) {
}
// Adds author details to each revision.
- const _modifiedRevisions = revisions.map( ( revision ) => {
+ const _modifiedRevisions: Revision[] = revisions.map( ( revision ) => {
return {
...revision,
author: authors.find(
@@ -115,14 +155,15 @@ export default function useGlobalStylesRevisions( { query } = {} ) {
currentUser &&
_query.page === 1
) {
- const unsavedRevision = {
+ const unsavedRevision: Revision = {
id: 'unsaved',
styles: userConfig?.styles,
settings: userConfig?.settings,
_links: userConfig?._links,
author: {
- name: currentUser?.name,
- avatar_urls: currentUser?.avatar_urls,
+ name: currentUser?.name || '',
+ // @ts-expect-error - avatar_urls is not typed in User
+ avatar_urls: currentUser?.avatar_urls || {},
},
modified: new Date(),
};
@@ -131,6 +172,7 @@ export default function useGlobalStylesRevisions( { query } = {} ) {
}
if (
+ _query.per_page &&
_query.page === Math.ceil( revisionsCount / _query.per_page )
) {
// Adds an item for the default theme styles.
@@ -155,5 +197,8 @@ export default function useGlobalStylesRevisions( { query } = {} ) {
authors,
userConfig,
isLoadingGlobalStylesRevisions,
+ revisionsCount,
+ _query.page,
+ _query.per_page,
] );
}
diff --git a/packages/edit-site/src/components/global-styles/screen-root.js b/packages/global-styles-ui/src/screen-root.tsx
similarity index 54%
rename from packages/edit-site/src/components/global-styles/screen-root.js
rename to packages/global-styles-ui/src/screen-root.tsx
index 10f982aa69fe0e..71ad700d80fc08 100644
--- a/packages/edit-site/src/components/global-styles/screen-root.js
+++ b/packages/global-styles-ui/src/screen-root.tsx
@@ -16,6 +16,7 @@ import { isRTL, __ } from '@wordpress/i18n';
import { chevronLeft, chevronRight } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
+import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
/**
* Internal dependencies
@@ -26,23 +27,39 @@ import RootMenu from './root-menu';
import PreviewStyles from './preview-styles';
function ScreenRoot() {
- const hasVariations = useSelect( ( select ) => {
- const { __experimentalGetCurrentThemeGlobalStylesVariations } =
- select( coreStore );
- return !! __experimentalGetCurrentThemeGlobalStylesVariations()?.length;
+ const { hasVariations, canEditCSS } = useSelect( ( select ) => {
+ const {
+ __experimentalGetCurrentThemeGlobalStylesVariations,
+ __experimentalGetCurrentGlobalStylesId,
+ getEntityRecord,
+ } = select( coreStore );
+
+ const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+ const globalStyles = globalStylesId
+ ? getEntityRecord( 'root', 'globalStyles', globalStylesId )
+ : undefined;
+
+ return {
+ hasVariations:
+ !! __experimentalGetCurrentThemeGlobalStylesVariations()
+ ?.length,
+ canEditCSS: !! ( globalStyles as GlobalStylesConfig )?._links?.[
+ 'wp:action-edit-css'
+ ],
+ };
}, [] );
return (
-
-
+
+
@@ -72,13 +89,8 @@ function ScreenRoot() {
{ __(
'Customize the appearance of specific blocks for the whole site.'
@@ -95,6 +107,38 @@ function ScreenRoot() {
+
+ { canEditCSS && (
+ <>
+
+
+
+ { __(
+ 'Add your own CSS to customize the appearance and layout of your site.'
+ ) }
+
+
+
+
+
+ { __( 'Additional CSS' ) }
+
+
+
+
+
+
+ >
+ ) }
);
}
diff --git a/packages/edit-site/src/components/global-styles/screen-shadows.js b/packages/global-styles-ui/src/screen-shadows.tsx
similarity index 100%
rename from packages/edit-site/src/components/global-styles/screen-shadows.js
rename to packages/global-styles-ui/src/screen-shadows.tsx
diff --git a/packages/global-styles-ui/src/screen-style-variations.tsx b/packages/global-styles-ui/src/screen-style-variations.tsx
new file mode 100644
index 00000000000000..37bf2170e8ae33
--- /dev/null
+++ b/packages/global-styles-ui/src/screen-style-variations.tsx
@@ -0,0 +1,36 @@
+/**
+ * WordPress dependencies
+ */
+import { Card, CardBody } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { ScreenHeader } from './screen-header';
+import { StyleVariationsContent } from './style-variations-content';
+
+function ScreenStyleVariations() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ScreenStyleVariations;
diff --git a/packages/edit-site/src/components/global-styles/screen-typography-element.js b/packages/global-styles-ui/src/screen-typography-element.tsx
similarity index 91%
rename from packages/edit-site/src/components/global-styles/screen-typography-element.js
rename to packages/global-styles-ui/src/screen-typography-element.tsx
index 8a8150e1578719..8d60f9d7a0f5c4 100644
--- a/packages/edit-site/src/components/global-styles/screen-typography-element.js
+++ b/packages/global-styles-ui/src/screen-typography-element.tsx
@@ -13,7 +13,7 @@ import { useState } from '@wordpress/element';
* Internal dependencies
*/
import TypographyPanel from './typography-panel';
-import ScreenHeader from './header';
+import { ScreenHeader } from './screen-header';
import TypographyPreview from './typography-preview';
const elements = {
@@ -39,7 +39,11 @@ const elements = {
},
};
-function ScreenTypographyElement( { element } ) {
+interface ScreenTypographyElementProps {
+ element: keyof typeof elements;
+}
+
+function ScreenTypographyElement( { element }: ScreenTypographyElementProps ) {
const [ headingLevel, setHeadingLevel ] = useState( 'heading' );
return (
@@ -60,7 +64,9 @@ function ScreenTypographyElement( { element } ) {
label={ __( 'Select heading level' ) }
hideLabelFromVision
value={ headingLevel }
- onChange={ setHeadingLevel }
+ onChange={ ( value ) =>
+ setHeadingLevel( value as string )
+ }
isBlock
size="__unstable-large"
__nextHasNoMarginBottom
diff --git a/packages/edit-site/src/components/global-styles/screen-typography.js b/packages/global-styles-ui/src/screen-typography.tsx
similarity index 73%
rename from packages/edit-site/src/components/global-styles/screen-typography.js
rename to packages/global-styles-ui/src/screen-typography.tsx
index 3739e3234258bd..2149ad99fb8d01 100644
--- a/packages/edit-site/src/components/global-styles/screen-typography.js
+++ b/packages/global-styles-ui/src/screen-typography.tsx
@@ -3,24 +3,20 @@
*/
import { __ } from '@wordpress/i18n';
import { __experimentalVStack as VStack } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-import { store as editorStore } from '@wordpress/editor';
+import { useContext } from '@wordpress/element';
/**
* Internal dependencies
*/
+import { ScreenHeader } from './screen-header';
import TypographyElements from './typography-elements';
-import ScreenHeader from './header';
import TypographyVariations from './variations/variations-typography';
-import FontSizesCount from './font-sizes/font-sizes-count';
import FontFamilies from './font-families';
+import FontSizesCount from './font-sizes/font-sizes-count';
+import { GlobalStylesContext } from './context';
function ScreenTypography() {
- const fontLibraryEnabled = useSelect(
- ( select ) =>
- select( editorStore ).getEditorSettings().fontLibraryEnabled,
- []
- );
+ const { fontLibraryEnabled } = useContext( GlobalStylesContext );
return (
<>
@@ -30,7 +26,7 @@ function ScreenTypography() {
'Available fonts, typographic styles, and the application of those styles.'
) }
/>
-
+
{ fontLibraryEnabled && }
diff --git a/packages/edit-site/src/components/global-styles/shadow-utils.js b/packages/global-styles-ui/src/shadow-utils.ts
similarity index 92%
rename from packages/edit-site/src/components/global-styles/shadow-utils.js
rename to packages/global-styles-ui/src/shadow-utils.ts
index a98f6306bb3f35..2173c8789d4fd7 100644
--- a/packages/edit-site/src/components/global-styles/shadow-utils.js
+++ b/packages/global-styles-ui/src/shadow-utils.ts
@@ -29,12 +29,21 @@ export const CUSTOM_VALUE_SETTINGS = {
dvmax: { max: 100, step: 1 },
};
-export function getShadowParts( shadow ) {
+export interface ShadowObject {
+ x: string;
+ y: string;
+ blur: string;
+ spread: string;
+ color: string;
+ inset: boolean;
+}
+
+export function getShadowParts( shadow: string ): string[] {
const shadowValues = shadow.match( /(?:[^,(]|\([^)]*\))+/g ) || [];
return shadowValues.map( ( value ) => value.trim() );
}
-export function shadowStringToObject( shadowValue ) {
+export function shadowStringToObject( shadowValue: string ): ShadowObject {
/*
* Shadow spec: https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow
* Shadow string format: [inset]
@@ -49,7 +58,7 @@ export function shadowStringToObject( shadowValue ) {
* 6. Should not contain more than one color value.
*/
- const defaultShadow = {
+ const defaultShadow: ShadowObject = {
x: '0',
y: '0',
blur: '0',
@@ -147,7 +156,7 @@ export function shadowStringToObject( shadowValue ) {
};
}
-export function shadowObjectToString( shadowObj ) {
+export function shadowObjectToString( shadowObj: ShadowObject ): string {
const shadowString = `${ shadowObj.x || '0px' } ${ shadowObj.y || '0px' } ${
shadowObj.blur || '0px'
} ${ shadowObj.spread || '0px' }`;
diff --git a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js b/packages/global-styles-ui/src/shadows-edit-panel.tsx
similarity index 78%
rename from packages/edit-site/src/components/global-styles/shadows-edit-panel.js
rename to packages/global-styles-ui/src/shadows-edit-panel.tsx
index ff6a3397165adc..5c67191d629d29 100644
--- a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js
+++ b/packages/global-styles-ui/src/shadows-edit-panel.tsx
@@ -28,7 +28,6 @@ import {
privateApis as componentsPrivateApis,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import {
plus,
shadow as shadowIcon,
@@ -40,17 +39,17 @@ import { useState, useMemo, useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { unlock } from '../../lock-unlock';
-import Subtitle from './subtitle';
-import ScreenHeader from './header';
+import { Subtitle } from './subtitle';
+import { ScreenHeader } from './screen-header';
import { defaultShadow } from './shadows-panel';
import {
getShadowParts,
shadowStringToObject,
shadowObjectToString,
} from './shadow-utils';
+import { useSetting } from './hooks';
+import { unlock } from './lock-unlock';
-const { useGlobalSetting } = unlock( blockEditorPrivateApis );
const { Menu } = unlock( componentsPrivateApis );
const customShadowMenuItems = [
@@ -72,17 +71,16 @@ const presetShadowMenuItems = [
];
export default function ShadowsEditPanel() {
- const {
- goBack,
- params: { category, slug },
- } = useNavigator();
- const [ shadows, setShadows ] = useGlobalSetting(
+ const { goBack, params } = useNavigator();
+ const { category, slug } = params;
+
+ const [ shadows, setShadows ] = useSetting(
`shadow.presets.${ category }`
);
useEffect( () => {
const hasCurrentShadow = shadows?.some(
- ( shadow ) => shadow.slug === slug
+ ( shadow: any ) => shadow.slug === slug
);
// If the shadow being edited doesn't exist anymore in the global styles setting, navigate back
// to prevent the user from editing a non-existent shadow entry.
@@ -97,38 +95,40 @@ export default function ShadowsEditPanel() {
}
}, [ shadows, slug, goBack ] );
- const [ baseShadows ] = useGlobalSetting(
+ const [ baseShadows ] = useSetting(
`shadow.presets.${ category }`,
undefined,
'base'
);
const [ selectedShadow, setSelectedShadow ] = useState( () =>
- ( shadows || [] ).find( ( shadow ) => shadow.slug === slug )
+ ( shadows || [] ).find( ( shadow: any ) => shadow.slug === slug )
);
const baseSelectedShadow = useMemo(
- () => ( baseShadows || [] ).find( ( b ) => b.slug === slug ),
+ () => ( baseShadows || [] ).find( ( b: any ) => b.slug === slug ),
[ baseShadows, slug ]
);
const [ isConfirmDialogVisible, setIsConfirmDialogVisible ] =
useState( false );
const [ isRenameModalVisible, setIsRenameModalVisible ] = useState( false );
- const [ shadowName, setShadowName ] = useState( selectedShadow.name );
+ const [ shadowName, setShadowName ] = useState< string | undefined >(
+ selectedShadow?.name
+ );
if ( ! category || ! slug ) {
return null;
}
- const onShadowChange = ( shadow ) => {
+ const onShadowChange = ( shadow: string ) => {
setSelectedShadow( { ...selectedShadow, shadow } );
- const updatedShadows = shadows.map( ( s ) =>
+ const updatedShadows = shadows.map( ( s: any ) =>
s.slug === slug ? { ...selectedShadow, shadow } : s
);
setShadows( updatedShadows );
};
- const onMenuClick = ( action ) => {
+ const onMenuClick = ( action: string ) => {
if ( action === 'reset' ) {
- const updatedShadows = shadows.map( ( s ) =>
+ const updatedShadows = shadows.map( ( s: any ) =>
s.slug === slug ? baseSelectedShadow : s
);
setSelectedShadow( baseSelectedShadow );
@@ -141,14 +141,14 @@ export default function ShadowsEditPanel() {
};
const handleShadowDelete = () => {
- setShadows( shadows.filter( ( s ) => s.slug !== slug ) );
+ setShadows( shadows.filter( ( s: any ) => s.slug !== slug ) );
};
- const handleShadowRename = ( newName ) => {
+ const handleShadowRename = ( newName: string | undefined ) => {
if ( ! newName ) {
return;
}
- const updatedShadows = shadows.map( ( s ) =>
+ const updatedShadows = shadows.map( ( s: any ) =>
s.slug === slug ? { ...selectedShadow, name: newName } : s
);
setSelectedShadow( { ...selectedShadow, name: newName } );
@@ -186,7 +186,7 @@ export default function ShadowsEditPanel() {
disabled={
item.action === 'reset' &&
selectedShadow.shadow ===
- baseSelectedShadow.shadow
+ baseSelectedShadow?.shadow
}
>
@@ -199,7 +199,7 @@ export default function ShadowsEditPanel() {
-
+
setShadowName( value ) }
+ value={ shadowName ?? '' }
+ onChange={ setShadowName }
/>
@@ -304,11 +308,16 @@ function ShadowsPreview( { shadow } ) {
);
}
-function ShadowEditor( { shadow, onChange } ) {
- const addShadowButtonRef = useRef();
+interface ShadowEditorProps {
+ shadow: string;
+ onChange: ( shadow: string ) => void;
+}
+
+function ShadowEditor( { shadow, onChange }: ShadowEditorProps ) {
+ const addShadowButtonRef = useRef< HTMLButtonElement >( null );
const shadowParts = useMemo( () => getShadowParts( shadow ), [ shadow ] );
- const onChangeShadowPart = ( index, part ) => {
+ const onChangeShadowPart = ( index: number, part: string ) => {
const newShadowParts = [ ...shadowParts ];
newShadowParts[ index ] = part;
onChange( newShadowParts.join( ', ' ) );
@@ -318,9 +327,9 @@ function ShadowEditor( { shadow, onChange } ) {
onChange( [ ...shadowParts, defaultShadow ].join( ', ' ) );
};
- const onRemoveShadowPart = ( index ) => {
+ const onRemoveShadowPart = ( index: number ) => {
onChange( shadowParts.filter( ( p, i ) => i !== index ).join( ', ' ) );
- addShadowButtonRef.current.focus();
+ addShadowButtonRef.current?.focus();
};
return (
@@ -328,7 +337,7 @@ function ShadowEditor( { shadow, onChange } ) {
{ __( 'Shadows' ) }
-
+
void;
+ canRemove: boolean;
+ onRemove: () => void;
+}
+
+function ShadowItem( {
+ shadow,
+ onChange,
+ canRemove,
+ onRemove,
+}: ShadowItemProps ) {
const popoverProps = {
- placement: 'left-start',
+ placement: 'left-start' as const,
offset: 36,
shift: true,
};
@@ -369,19 +390,19 @@ function ShadowItem( { shadow, onChange, canRemove, onRemove } ) {
() => shadowStringToObject( shadow ),
[ shadow ]
);
- const onShadowChange = ( newShadow ) => {
+ const onShadowChange = ( newShadow: any ) => {
onChange( shadowObjectToString( newShadow ) );
};
return (
{
const toggleProps = {
onClick: onToggle,
className: clsx(
- 'edit-site-global-styles__shadow-editor__dropdown-toggle',
+ 'global-styles-ui__shadow-editor__dropdown-toggle',
{ 'is-open': isOpen }
),
'aria-expanded': isOpen,
@@ -394,7 +415,7 @@ function ShadowItem( { shadow, onChange, canRemove, onRemove } ) {
onRemove();
},
className: clsx(
- 'edit-site-global-styles__shadow-editor__remove-button',
+ 'global-styles-ui__shadow-editor__remove-button',
{ 'is-open': isOpen }
),
label: __( 'Remove shadow' ),
@@ -424,7 +445,7 @@ function ShadowItem( { shadow, onChange, canRemove, onRemove } ) {
renderContent={ () => (
void;
+}
+
+function ShadowPopover( { shadowObj, onChange }: ShadowPopoverProps ) {
const __experimentalIsRenderedInSidebar = true;
const enableAlpha = true;
- const onShadowChange = ( key, value ) => {
+ const onShadowChange = ( key: string, value: any ) => {
const newShadow = {
...shadowObj,
[ key ]: value,
@@ -449,10 +475,7 @@ function ShadowPopover( { shadowObj, onChange } ) {
};
return (
-
+
onShadowChange( 'color', value ) }
/>
{
+interface ShadowInputControlProps {
+ label: string;
+ value: string;
+ onChange: ( value: string ) => void;
+}
+
+function ShadowInputControl( {
+ label,
+ value,
+ onChange,
+}: ShadowInputControlProps ) {
+ const onValueChange = ( next: string | undefined ) => {
const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) );
const nextValue = isNumeric ? next : '0px';
onChange( nextValue );
diff --git a/packages/edit-site/src/components/global-styles/shadows-panel.js b/packages/global-styles-ui/src/shadows-panel.tsx
similarity index 78%
rename from packages/edit-site/src/components/global-styles/shadows-panel.js
rename to packages/global-styles-ui/src/shadows-panel.tsx
index 66dd602fd94ed9..ae5e7f33953551 100644
--- a/packages/edit-site/src/components/global-styles/shadows-panel.js
+++ b/packages/global-styles-ui/src/shadows-panel.tsx
@@ -10,7 +10,6 @@ import {
privateApis as componentsPrivateApis,
} from '@wordpress/components';
import { __, sprintf, isRTL } from '@wordpress/i18n';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import {
plus,
Icon,
@@ -18,34 +17,32 @@ import {
chevronRight,
moreVertical,
} from '@wordpress/icons';
+import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { unlock } from '../../lock-unlock';
-import Subtitle from './subtitle';
+import { Subtitle } from './subtitle';
import { NavigationButtonAsItem } from './navigation-button';
-import ScreenHeader from './header';
+import { ScreenHeader } from './screen-header';
import { getNewIndexFromPresets } from './utils';
-import { useState } from '@wordpress/element';
import ConfirmResetShadowDialog from './confirm-reset-shadow-dialog';
+import { useSetting } from './hooks';
+import { unlock } from './lock-unlock';
-const { useGlobalSetting } = unlock( blockEditorPrivateApis );
const { Menu } = unlock( componentsPrivateApis );
export const defaultShadow = '6px 6px 9px rgba(0, 0, 0, 0.2)';
export default function ShadowsPanel() {
- const [ defaultShadows ] = useGlobalSetting( 'shadow.presets.default' );
- const [ defaultShadowsEnabled ] = useGlobalSetting(
- 'shadow.defaultPresets'
- );
- const [ themeShadows ] = useGlobalSetting( 'shadow.presets.theme' );
- const [ customShadows, setCustomShadows ] = useGlobalSetting(
+ const [ defaultShadows ] = useSetting( 'shadow.presets.default' );
+ const [ defaultShadowsEnabled ] = useSetting( 'shadow.defaultPresets' );
+ const [ themeShadows ] = useSetting( 'shadow.presets.theme' );
+ const [ customShadows, setCustomShadows ] = useSetting(
'shadow.presets.custom'
);
- const onCreateShadow = ( shadow ) => {
+ const onCreateShadow = ( shadow: any ) => {
setCustomShadows( [ ...( customShadows || [] ), shadow ] );
};
@@ -76,9 +73,9 @@ export default function ShadowsPanel() {
'Manage and create shadow styles for use across the site.'
) }
/>
-
+
{ defaultShadowsEnabled && (
@@ -109,6 +106,15 @@ export default function ShadowsPanel() {
);
}
+interface ShadowListProps {
+ label: string;
+ shadows: any[];
+ category: string;
+ canCreate?: boolean;
+ onCreate?: ( shadow: any ) => void;
+ onReset?: () => void;
+}
+
function ShadowList( {
label,
shadows,
@@ -116,13 +122,13 @@ function ShadowList( {
canCreate,
onCreate,
onReset,
-} ) {
+}: ShadowListProps ) {
const handleAddShadow = () => {
const newIndex = getNewIndexFromPresets( shadows, 'shadow-' );
- onCreate( {
+ onCreate?.( {
name: sprintf(
- /* translators: %s: is an index for a preset */
- __( 'Shadow %s' ),
+ /* translators: %d: is an index for a preset */
+ __( 'Shadow %d' ),
newIndex
),
shadow: defaultShadow,
@@ -134,7 +140,7 @@ function ShadowList( {
{ label }
-
+
{ canCreate && (
void;
+ fallbackValue?: number;
+ disabled?: boolean;
+ label?: string;
+ __nextHasNoMarginBottom?: boolean;
+}
+
function SizeControl( {
// Do not allow manipulation of margin bottom
__nextHasNoMarginBottom,
...props
-} ) {
+}: SizeControlProps ) {
const { baseControlProps } = useBaseControlProps( props );
const { value, onChange, fallbackValue, disabled, label } = props;
@@ -38,13 +43,17 @@ function SizeControl( {
!! valueUnit && [ 'em', 'rem', 'vw', 'vh' ].includes( valueUnit );
// Receives the new value from the UnitControl component as a string containing the value and unit.
- const handleUnitControlChange = ( newValue ) => {
- onChange( newValue );
+ const handleUnitControlChange = ( newValue: string | undefined ) => {
+ onChange?.( newValue );
};
// Receives the new value from the RangeControl component as a number.
- const handleRangeControlChange = ( newValue ) => {
- onChange?.( newValue + valueUnit );
+ const handleRangeControlChange = ( newValue: number | undefined ) => {
+ if ( newValue !== undefined ) {
+ onChange?.( newValue + valueUnit );
+ } else {
+ onChange?.( undefined );
+ }
};
return (
@@ -53,7 +62,6 @@ function SizeControl( {
{
settings: {},
styles: {},
} );
- const context = useMemo( () => {
- return {
- isReady: true,
- user: userGlobalStyles,
- base: BASE_SETTINGS,
- merged: mergeBaseAndUserConfigs( BASE_SETTINGS, userGlobalStyles ),
- setUserConfig: setUserStyles,
- };
- }, [ userGlobalStyles, setUserStyles ] );
const wrapperStyle = {
width: 280,
};
return (
-
-
-
- {} }
- />
-
-
-
+
+
+
);
};
diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/global-styles-ui/src/style-variations-container.tsx
similarity index 56%
rename from packages/edit-site/src/components/global-styles/style-variations-container.js
rename to packages/global-styles-ui/src/style-variations-container.tsx
index 759303bcf5fa7b..ec58ef35dc8946 100644
--- a/packages/edit-site/src/components/global-styles/style-variations-container.js
+++ b/packages/global-styles-ui/src/style-variations-container.tsx
@@ -6,41 +6,55 @@ import { useSelect } from '@wordpress/data';
import { useContext, useMemo } from '@wordpress/element';
import { __experimentalGrid as Grid } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import type {
+ GlobalStylesConfig,
+ StyleVariation,
+} from '@wordpress/global-styles-engine';
/**
* Internal dependencies
*/
import PreviewStyles from './preview-styles';
import Variation from './variations/variation';
-import { isVariationWithProperties } from '../../hooks/use-theme-style-variations/use-theme-style-variations-by-property';
-import { unlock } from '../../lock-unlock';
+import { GlobalStylesContext } from './context';
+import { isVariationWithProperties } from './utils';
-const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
+interface StyleVariationsContainerProps {
+ gap?: number;
+}
-export default function StyleVariationsContainer( { gap = 2 } ) {
+function StyleVariationsContainer( {
+ gap = 2,
+}: StyleVariationsContainerProps ) {
const { user } = useContext( GlobalStylesContext );
const userStyles = user?.styles;
const variations = useSelect( ( select ) => {
- return select(
- coreStore
- ).__experimentalGetCurrentThemeGlobalStylesVariations();
+ const result =
+ select(
+ coreStore
+ ).__experimentalGetCurrentThemeGlobalStylesVariations();
+ // The API might return null or an array
+ return Array.isArray( result )
+ ? ( result as GlobalStylesConfig[] )
+ : undefined;
}, [] );
// Filter out variations that are color or typography variations.
- const fullStyleVariations = variations?.filter( ( variation ) => {
- return (
- ! isVariationWithProperties( variation, [ 'color' ] ) &&
- ! isVariationWithProperties( variation, [
- 'typography',
- 'spacing',
- ] )
- );
- } );
+ const fullStyleVariations = variations?.filter(
+ ( variation: GlobalStylesConfig ) => {
+ return (
+ ! isVariationWithProperties( variation, [ 'color' ] ) &&
+ ! isVariationWithProperties( variation, [
+ 'typography',
+ 'spacing',
+ ] )
+ );
+ }
+ );
const themeVariations = useMemo( () => {
- const withEmptyVariation = [
+ const withEmptyVariation: StyleVariation[] = [
{
title: __( 'Default' ),
settings: {},
@@ -49,8 +63,10 @@ export default function StyleVariationsContainer( { gap = 2 } ) {
...( fullStyleVariations ?? [] ),
];
return [
- ...withEmptyVariation.map( ( variation ) => {
- const blockStyles = { ...variation?.styles?.blocks } || {};
+ ...withEmptyVariation.map( ( variation: StyleVariation ) => {
+ const blockStyles = variation?.styles?.blocks
+ ? { ...variation.styles.blocks }
+ : {};
// We need to copy any user custom CSS to the variation to prevent it being lost
// when switching variations.
@@ -58,15 +74,16 @@ export default function StyleVariationsContainer( { gap = 2 } ) {
Object.keys( userStyles.blocks ).forEach( ( blockName ) => {
// First get any block specific custom CSS from the current user styles and merge with any custom CSS for
// that block in the variation.
- if ( userStyles.blocks[ blockName ].css ) {
+ if ( userStyles.blocks?.[ blockName ]?.css ) {
const variationBlockStyles =
blockStyles[ blockName ] || {};
const customCSS = {
css: `${
blockStyles[ blockName ]?.css || ''
} ${
- userStyles.blocks[ blockName ].css.trim() ||
- ''
+ userStyles.blocks?.[
+ blockName
+ ]?.css?.trim() || ''
}`,
};
blockStyles[ blockName ] = {
@@ -105,28 +122,32 @@ export default function StyleVariationsContainer( { gap = 2 } ) {
];
}, [ fullStyleVariations, userStyles?.blocks, userStyles?.css ] );
- if ( ! fullStyleVariations || fullStyleVariations?.length < 1 ) {
+ if ( ! fullStyleVariations || fullStyleVariations.length < 1 ) {
return null;
}
return (
- { themeVariations.map( ( variation, index ) => (
-
- { ( isFocused ) => (
-
- ) }
-
- ) ) }
+ { themeVariations.map(
+ ( variation: StyleVariation, index: number ) => (
+
+ { ( isFocused: boolean ) => (
+
+ ) }
+
+ )
+ ) }
);
}
+
+export default StyleVariationsContainer;
diff --git a/packages/global-styles-ui/src/style-variations-content.tsx b/packages/global-styles-ui/src/style-variations-content.tsx
new file mode 100644
index 00000000000000..6f7df3d2660d3e
--- /dev/null
+++ b/packages/global-styles-ui/src/style-variations-content.tsx
@@ -0,0 +1,24 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { __experimentalVStack as VStack } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import StyleVariationsContainer from './style-variations-container';
+import TypographyVariations from './variations/variations-typography';
+import ColorVariations from './variations/variations-color';
+
+export function StyleVariationsContent() {
+ const gap = 3;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/global-styles-ui/src/style-variations.tsx b/packages/global-styles-ui/src/style-variations.tsx
new file mode 100644
index 00000000000000..946b37a741d9cb
--- /dev/null
+++ b/packages/global-styles-ui/src/style-variations.tsx
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
+
+/**
+ * Internal dependencies
+ */
+import StyleVariationsContainer from './style-variations-container';
+import { withGlobalStylesProvider } from './with-global-styles-provider';
+
+export interface StyleVariationsProps {
+ value: GlobalStylesConfig;
+ baseValue: GlobalStylesConfig;
+ onChange: ( config: GlobalStylesConfig ) => void;
+ gap?: number;
+}
+
+/**
+ * Render Style Variations.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const StyleVariations: React.ComponentType< StyleVariationsProps > =
+ withGlobalStylesProvider( StyleVariationsContainer );
diff --git a/packages/global-styles-ui/src/style.scss b/packages/global-styles-ui/src/style.scss
new file mode 100644
index 00000000000000..7522633f582531
--- /dev/null
+++ b/packages/global-styles-ui/src/style.scss
@@ -0,0 +1,287 @@
+@use "@wordpress/base-styles/variables";
+@use "@wordpress/base-styles/colors";
+@use "@wordpress/base-styles/mixins";
+@use "./font-library-modal/style.scss" as *;
+@use "./pagination/style.scss" as *;
+@use "./screen-revisions/style.scss" as *;
+@use "./size-control/style.scss" as *;
+@use "./variations/style.scss" as *;
+
+
+.global-styles-ui-preview {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.global-styles-ui-preview__wrapper {
+ max-width: 100%;
+ display: block;
+ width: 100%;
+}
+
+.global-styles-ui-typography-preview {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100px;
+ margin-bottom: 20px;
+ background: #f0f0f0;
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.global-styles-ui-font-size__item {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ line-break: anywhere;
+}
+
+.global-styles-ui-font-size__item-value {
+ color: #757575;
+}
+
+.global-styles-ui-screen {
+ margin: 12px 16px 16px;
+}
+
+.global-styles-ui-screen-typography__indicator {
+ height: 24px;
+ width: 24px;
+ font-size: 14px;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+}
+
+.global-styles-ui-block-types-search {
+ margin-bottom: 10px;
+ padding: 0 20px;
+}
+
+.global-styles-ui-screen-typography__font-variants-count {
+ color: colors.$gray-700;
+}
+
+.global-styles-ui-font-families__manage-fonts {
+ justify-content: center;
+}
+
+.global-styles-ui-screen .color-block-support-panel {
+ padding-left: 0;
+ padding-right: 0;
+ padding-top: 0;
+ border-top: none;
+ row-gap: calc(4px * 3);
+}
+
+.global-styles-ui-header__description {
+ padding: 0 16px;
+}
+
+.global-styles-ui-header {
+ // Need to override the too specific bottom margin for complementary areas.
+ margin-bottom: 0 !important;
+}
+
+.global-styles-ui-subtitle {
+ // Need to override the too specific styles for complementary areas.
+ margin-bottom: 0 !important;
+ text-transform: uppercase;
+ font-weight: variables.$font-weight-medium !important;
+ font-size: 11px !important;
+}
+
+.global-styles-ui-section-title {
+ color: colors.$gray-800;
+ font-weight: 600;
+ line-height: 1.2;
+ padding: 16px 16px 0;
+ margin: 0;
+}
+
+.global-styles-ui-icon-with-current-color {
+ fill: currentColor;
+}
+
+.global-styles-ui__color-indicator-wrapper {
+ // Match the height of the rest of the icons (24px).
+ height: variables.$grid-unit * 3;
+ flex-shrink: 0;
+}
+
+.global-styles-ui__shadows-panel__options-container,
+.global-styles-ui__typography-panel__options-container {
+ height: variables.$grid-unit * 3;
+}
+
+.global-styles-ui__block-preview-panel {
+ position: relative;
+ width: 100%;
+ border: #ddd 1px solid;
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.global-styles-ui__shadow-preview-panel {
+ height: 144px;
+ border: #ddd 1px solid;
+ border-radius: 2px;
+ overflow: auto;
+ background-image:
+ repeating-linear-gradient(45deg, #f5f5f5 25%, #0000 0, #0000 75%, #f5f5f5 0, #f5f5f5),
+ repeating-linear-gradient(45deg, #f5f5f5 25%, #0000 0, #0000 75%, #f5f5f5 0, #f5f5f5);
+ background-position:
+ 0 0,
+ 8px 8px;
+ background-size: 16px 16px;
+
+ .global-styles-ui__shadow-preview-block {
+ border: #ddd 1px solid;
+ border-radius: 2px;
+ background-color: #fff;
+ width: 60%;
+ height: 60px;
+ }
+}
+
+.global-styles-ui__shadow-editor__dropdown-content {
+ width: 280px;
+}
+
+.global-styles-ui__shadow-editor-panel {
+ // because tooltip of the range control is too close to the edge and creates overflow
+ margin-bottom: 4px;
+}
+
+.global-styles-ui__shadow-editor__dropdown {
+ width: 100%;
+ position: relative;
+}
+
+.global-styles-ui__shadow-editor__dropdown-toggle {
+ width: 100%;
+ height: auto;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ text-align: left;
+ border-radius: inherit;
+
+ &.is-open {
+ background: colors.$gray-100;
+ color: var(--wp-admin-theme-color);
+ }
+}
+
+.global-styles-ui__shadow-editor__remove-button {
+ position: absolute;
+ right: 8px;
+ top: 8px;
+ opacity: 0;
+
+ &.global-styles-ui__shadow-editor__remove-button {
+ border: none;
+ }
+
+ .global-styles-ui__shadow-editor__dropdown-toggle:hover + &,
+ &:focus,
+ &:hover {
+ opacity: 1;
+ }
+
+ @media ( hover: none ) {
+ // Show reset button on devices that do not support hover.
+ opacity: 1;
+ }
+}
+
+.global-styles-ui-screen-css {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ margin: 16px;
+
+ .components-v-stack {
+ flex: 1 1 auto;
+
+ .block-editor-global-styles-advanced-panel__custom-css-input {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+
+ .components-base-control__field {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+
+ .components-textarea-control__input {
+ flex: 1 1 auto;
+ // CSS input is always LTR regardless of language.
+
+ /*rtl:ignore*/
+ direction: ltr;
+ }
+ }
+ }
+ }
+}
+
+.global-styles-ui-screen-css-help-link {
+ display: inline-block;
+ margin-top: 8px;
+}
+
+.global-styles-ui-screen-variations {
+ margin-top: 16px;
+ border-top: 1px solid #ddd;
+
+ > * {
+ margin: 24px 16px;
+ }
+}
+
+.global-styles-ui-sidebar__navigator-provider {
+ height: 100%;
+}
+
+.global-styles-ui-sidebar__navigator-screen {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.global-styles-ui-sidebar__navigator-screen .single-column {
+ grid-column: span 1;
+}
+
+.global-styles-ui-screen-root.global-styles-ui-screen-root,
+.global-styles-ui-screen-style-variations.global-styles-ui-screen-style-variations {
+ background: unset;
+ color: inherit;
+}
+
+.global-styles-ui-sidebar__panel .block-editor-block-icon svg {
+ fill: currentColor;
+}
+
+.global-styles-ui-screen-root__active-style-tile {
+ // Todo: revisit use of Card component and/or remove override
+ // The {&} is a workaround for the specificity of the Card component.
+ {&},
+ {&} .global-styles-ui-screen-root__active-style-tile-preview {
+ border-radius: 2px;
+ }
+}
+
+.global-styles-ui-color-palette-panel,
+.global-styles-ui-gradient-palette-panel {
+ padding: variables.$grid-unit-20;
+}
+
+.components-navigator-screen {
+ padding: variables.$grid-unit-15;
+}
diff --git a/packages/global-styles-ui/src/subtitle.tsx b/packages/global-styles-ui/src/subtitle.tsx
new file mode 100644
index 00000000000000..eb861893f3aabc
--- /dev/null
+++ b/packages/global-styles-ui/src/subtitle.tsx
@@ -0,0 +1,17 @@
+/**
+ * WordPress dependencies
+ */
+import { __experimentalHeading as Heading } from '@wordpress/components';
+
+interface SubtitleProps {
+ children: React.ReactNode;
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
+}
+
+export function Subtitle( { children, level = 2 }: SubtitleProps ) {
+ return (
+
+ { children }
+
+ );
+}
diff --git a/packages/edit-site/src/components/global-styles/test/shadow-utils.spec.js b/packages/global-styles-ui/src/test/shadow-utils.spec.js
similarity index 100%
rename from packages/edit-site/src/components/global-styles/test/shadow-utils.spec.js
rename to packages/global-styles-ui/src/test/shadow-utils.spec.js
diff --git a/packages/edit-site/src/components/global-styles/test/utils.spec.js b/packages/global-styles-ui/src/test/utils.spec.js
similarity index 100%
rename from packages/edit-site/src/components/global-styles/test/utils.spec.js
rename to packages/global-styles-ui/src/test/utils.spec.js
diff --git a/packages/edit-site/src/components/global-styles/typography-elements.js b/packages/global-styles-ui/src/typography-elements.tsx
similarity index 67%
rename from packages/edit-site/src/components/global-styles/typography-elements.js
rename to packages/global-styles-ui/src/typography-elements.tsx
index d087c8df041521..968bfcccd29b7a 100644
--- a/packages/edit-site/src/components/global-styles/typography-elements.js
+++ b/packages/global-styles-ui/src/typography-elements.tsx
@@ -8,18 +8,21 @@ import {
__experimentalHStack as HStack,
FlexItem,
} from '@wordpress/components';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { NavigationButtonAsItem } from './navigation-button';
-import Subtitle from './subtitle';
+import { Subtitle } from './subtitle';
+import { useStyle } from './hooks';
-import { unlock } from '../../lock-unlock';
-const { useGlobalStyle } = unlock( blockEditorPrivateApis );
+interface ElementItemProps {
+ parentMenu: string;
+ element: string;
+ label: string;
+}
-function ElementItem( { parentMenu, element, label } ) {
+function ElementItem( { parentMenu, element, label }: ElementItemProps ) {
const prefix =
element === 'text' || ! element ? '' : `elements.${ element }.`;
const extraStyles =
@@ -28,19 +31,28 @@ function ElementItem( { parentMenu, element, label } ) {
textDecoration: 'underline',
}
: {};
- const [ fontFamily ] = useGlobalStyle( prefix + 'typography.fontFamily' );
- const [ fontStyle ] = useGlobalStyle( prefix + 'typography.fontStyle' );
- const [ fontWeight ] = useGlobalStyle( prefix + 'typography.fontWeight' );
- const [ backgroundColor ] = useGlobalStyle( prefix + 'color.background' );
- const [ fallbackBackgroundColor ] = useGlobalStyle( 'color.background' );
- const [ gradientValue ] = useGlobalStyle( prefix + 'color.gradient' );
- const [ color ] = useGlobalStyle( prefix + 'color.text' );
+
+ const [ fontFamily ] = useStyle< string >(
+ prefix + 'typography.fontFamily'
+ );
+ const [ fontStyle ] = useStyle< string >( prefix + 'typography.fontStyle' );
+ const [ fontWeight ] = useStyle< string >(
+ prefix + 'typography.fontWeight'
+ );
+ const [ backgroundColor ] = useStyle< string >(
+ prefix + 'color.background'
+ );
+ const [ fallbackBackgroundColor ] =
+ useStyle< string >( 'color.background' );
+ const [ gradientValue ] = useStyle< string >( prefix + 'color.gradient' );
+ const [ color ] = useStyle< string >( prefix + 'color.text' );
return (
{ __( 'Aa' ) }
diff --git a/packages/edit-site/src/components/global-styles/typography-example.js b/packages/global-styles-ui/src/typography-example.tsx
similarity index 67%
rename from packages/edit-site/src/components/global-styles/typography-example.js
rename to packages/global-styles-ui/src/typography-example.tsx
index 9c0a4e0e1cb13a..0db5c79c000982 100644
--- a/packages/edit-site/src/components/global-styles/typography-example.js
+++ b/packages/global-styles-ui/src/typography-example.tsx
@@ -4,35 +4,37 @@
import { useContext } from '@wordpress/element';
import { __unstableMotion as motion } from '@wordpress/components';
import { _x } from '@wordpress/i18n';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
-import { privateApis as editorPrivateApis } from '@wordpress/editor';
/**
* Internal dependencies
*/
-import { unlock } from '../../lock-unlock';
+import { GlobalStylesContext } from './context';
import { getFamilyPreviewStyle } from './font-library-modal/utils/preview-styles';
import { getFontFamilies } from './utils';
+import { useStyle } from './hooks';
-const { useGlobalStyle, GlobalStylesContext } = unlock(
- blockEditorPrivateApis
-);
-const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis );
+interface TypographyExampleProps {
+ fontSize?: number;
+ variation?: any;
+}
-export default function PreviewTypography( { fontSize, variation } ) {
+export default function PreviewTypography( {
+ fontSize,
+ variation,
+}: TypographyExampleProps ) {
const { base } = useContext( GlobalStylesContext );
let config = base;
if ( variation ) {
- config = mergeBaseAndUserConfigs( base, variation );
+ config = { ...base, ...variation };
}
- const [ textColor ] = useGlobalStyle( 'color.text' );
+ const [ textColor ] = useStyle( 'color.text' );
const [ bodyFontFamilies, headingFontFamilies ] = getFontFamilies( config );
- const bodyPreviewStyle = bodyFontFamilies
+ const bodyPreviewStyle: React.CSSProperties = bodyFontFamilies
? getFamilyPreviewStyle( bodyFontFamilies )
: {};
- const headingPreviewStyle = headingFontFamilies
+ const headingPreviewStyle: React.CSSProperties = headingFontFamilies
? getFamilyPreviewStyle( headingFontFamilies )
: {};
diff --git a/packages/edit-site/src/components/global-styles/typography-panel.js b/packages/global-styles-ui/src/typography-panel.tsx
similarity index 54%
rename from packages/edit-site/src/components/global-styles/typography-panel.js
rename to packages/global-styles-ui/src/typography-panel.tsx
index 795a101bed839f..deca98cb792f2a 100644
--- a/packages/edit-site/src/components/global-styles/typography-panel.js
+++ b/packages/global-styles-ui/src/typography-panel.tsx
@@ -1,22 +1,28 @@
/**
* WordPress dependencies
*/
+// @ts-expect-error: Not typed yet.
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
-import { unlock } from '../../lock-unlock';
+import { useStyle, useSetting } from './hooks';
+import { unlock } from './lock-unlock';
-const {
- useGlobalStyle,
- useGlobalSetting,
- useSettingsForBlockElement,
- TypographyPanel: StylesTypographyPanel,
-} = unlock( blockEditorPrivateApis );
+const { useSettingsForBlockElement, TypographyPanel: StylesTypographyPanel } =
+ unlock( blockEditorPrivateApis );
-export default function TypographyPanel( { element, headingLevel } ) {
- let prefixParts = [];
+interface TypographyPanelProps {
+ element: string;
+ headingLevel: string;
+}
+
+export default function TypographyPanel( {
+ element,
+ headingLevel,
+}: TypographyPanelProps ) {
+ let prefixParts: string[] = [];
if ( element === 'heading' ) {
prefixParts = prefixParts.concat( [ 'elements', headingLevel ] );
} else if ( element && element !== 'text' ) {
@@ -24,18 +30,14 @@ export default function TypographyPanel( { element, headingLevel } ) {
}
const prefix = prefixParts.join( '.' );
- const [ style ] = useGlobalStyle( prefix, undefined, 'user', {
- shouldDecodeEncode: false,
- } );
- const [ inheritedStyle, setStyle ] = useGlobalStyle(
+ const [ style ] = useStyle( prefix, '', 'user', false );
+ const [ inheritedStyle, setStyle ] = useStyle(
prefix,
- undefined,
- 'all',
- {
- shouldDecodeEncode: false,
- }
+ '',
+ 'merged',
+ false
);
- const [ rawSettings ] = useGlobalSetting( '' );
+ const [ rawSettings ] = useSetting( '' );
const usedElement = element === 'heading' ? headingLevel : element;
const settings = useSettingsForBlockElement(
rawSettings,
diff --git a/packages/global-styles-ui/src/typography-preview.tsx b/packages/global-styles-ui/src/typography-preview.tsx
new file mode 100644
index 00000000000000..5909bc08e39b2a
--- /dev/null
+++ b/packages/global-styles-ui/src/typography-preview.tsx
@@ -0,0 +1,61 @@
+/**
+ * Internal dependencies
+ */
+import { useStyle } from './hooks';
+
+interface TypographyPreviewProps {
+ name?: string;
+ element: string;
+ headingLevel: string;
+}
+
+export default function TypographyPreview( {
+ name,
+ element,
+ headingLevel,
+}: TypographyPreviewProps ) {
+ let prefix = '';
+ if ( element === 'heading' ) {
+ prefix = `elements.${ headingLevel }.`;
+ } else if ( element && element !== 'text' ) {
+ prefix = `elements.${ element }.`;
+ }
+
+ const [ fontFamily ] = useStyle( prefix + 'typography.fontFamily', name );
+ const [ gradientValue ] = useStyle( prefix + 'color.gradient', name );
+ const [ backgroundColor ] = useStyle( prefix + 'color.background', name );
+ const [ fallbackBackgroundColor ] = useStyle( 'color.background' );
+ const [ color ] = useStyle( prefix + 'color.text', name );
+ const [ fontSize ] = useStyle( prefix + 'typography.fontSize', name );
+ const [ fontStyle ] = useStyle( prefix + 'typography.fontStyle', name );
+ const [ fontWeight ] = useStyle( prefix + 'typography.fontWeight', name );
+ const [ letterSpacing ] = useStyle(
+ prefix + 'typography.letterSpacing',
+ name
+ );
+ const extraStyles =
+ element === 'link'
+ ? {
+ textDecoration: 'underline',
+ }
+ : {};
+
+ return (
+
+ Aa
+
+ );
+}
diff --git a/packages/global-styles-ui/src/typography-variations.tsx b/packages/global-styles-ui/src/typography-variations.tsx
new file mode 100644
index 00000000000000..4a7e9e63b0e59b
--- /dev/null
+++ b/packages/global-styles-ui/src/typography-variations.tsx
@@ -0,0 +1,35 @@
+/**
+ * WordPress dependencies
+ */
+import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
+
+/**
+ * Internal dependencies
+ */
+import TypographyVariationsInternal from './variations/variations-typography';
+import { withGlobalStylesProvider } from './with-global-styles-provider';
+
+export interface TypographyVariationsProps {
+ value: GlobalStylesConfig;
+ baseValue: GlobalStylesConfig;
+ onChange: ( config: GlobalStylesConfig ) => void;
+ title?: string;
+ gap?: number;
+}
+
+/**
+ * Render Typography Variations.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const TypographyVariations: React.ComponentType< TypographyVariationsProps > =
+ withGlobalStylesProvider( TypographyVariationsInternal );
diff --git a/packages/global-styles-ui/src/utils.ts b/packages/global-styles-ui/src/utils.ts
new file mode 100644
index 00000000000000..4186c5fdc1144c
--- /dev/null
+++ b/packages/global-styles-ui/src/utils.ts
@@ -0,0 +1,325 @@
+/**
+ * External dependencies
+ */
+import fastDeepEqual from 'fast-deep-equal/es6';
+
+/**
+ * WordPress dependencies
+ */
+import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
+
+/**
+ * Removes all instances of properties from an object.
+ *
+ * @param object The object to remove the properties from.
+ * @param properties The properties to remove.
+ * @return The modified object.
+ */
+export function removePropertiesFromObject(
+ object: any,
+ properties: string[]
+): any {
+ if ( ! properties?.length ) {
+ return object;
+ }
+
+ if (
+ typeof object !== 'object' ||
+ ! object ||
+ ! Object.keys( object ).length
+ ) {
+ return object;
+ }
+
+ for ( const key in object ) {
+ if ( properties.includes( key ) ) {
+ delete object[ key ];
+ } else if ( typeof object[ key ] === 'object' ) {
+ removePropertiesFromObject( object[ key ], properties );
+ }
+ }
+ return object;
+}
+
+/**
+ * Returns a new object, with properties specified in `properties` array.,
+ * maintain the original object tree structure.
+ * The function is recursive, so it will perform a deep search for the given properties.
+ * E.g., the function will return `{ a: { b: { c: { test: 1 } } } }` if the properties are `[ 'test' ]`.
+ *
+ * @param object The object to filter
+ * @param properties The properties to filter by
+ * @return The merged object.
+ */
+export const filterObjectByProperties = (
+ object: any,
+ properties: string[]
+): any => {
+ if ( ! object || ! properties?.length ) {
+ return {};
+ }
+
+ const newObject: any = {};
+ Object.keys( object ).forEach( ( key ) => {
+ if ( properties.includes( key ) ) {
+ newObject[ key ] = object[ key ];
+ } else if ( typeof object[ key ] === 'object' ) {
+ const newFilter = filterObjectByProperties(
+ object[ key ],
+ properties
+ );
+ if ( Object.keys( newFilter ).length ) {
+ newObject[ key ] = newFilter;
+ }
+ }
+ } );
+ return newObject;
+};
+
+/**
+ * Compares global style variations according to their styles and settings properties.
+ *
+ * @param original A global styles object.
+ * @param variation A global styles object.
+ * @return Whether `original` and `variation` match.
+ */
+export function areGlobalStyleConfigsEqual(
+ original: any,
+ variation: any
+): boolean {
+ if ( typeof original !== 'object' || typeof variation !== 'object' ) {
+ return original === variation;
+ }
+ return (
+ fastDeepEqual( original?.styles, variation?.styles ) &&
+ fastDeepEqual( original?.settings, variation?.settings )
+ );
+}
+
+/**
+ * Compares a style variation to the same variation filtered by the specified properties.
+ * Returns true if the variation contains only the properties specified.
+ *
+ * @param variation The variation to compare.
+ * @param properties The properties to compare.
+ * @return Whether the variation contains only the specified properties.
+ */
+export function isVariationWithProperties(
+ variation: GlobalStylesConfig,
+ properties: string[]
+): boolean {
+ const variationWithProperties = filterObjectByProperties(
+ structuredClone( variation ),
+ properties
+ );
+
+ return areGlobalStyleConfigsEqual( variationWithProperties, variation );
+}
+
+function getFontFamilyFromSetting( fontFamilies: any[], setting: string ): any {
+ if ( ! Array.isArray( fontFamilies ) || ! setting ) {
+ return null;
+ }
+
+ const fontFamilyVariable = setting.replace( 'var(', '' ).replace( ')', '' );
+ const fontFamilySlug = fontFamilyVariable?.split( '--' ).slice( -1 )[ 0 ];
+
+ return fontFamilies.find(
+ ( fontFamily ) => fontFamily.slug === fontFamilySlug
+ );
+}
+
+/**
+ * Extracts font families from a theme JSON configuration.
+ *
+ * @param themeJson The theme JSON configuration
+ * @return Array containing [bodyFontFamily, headingFontFamily]
+ */
+export function getFontFamilies( themeJson: any ): [ any, any ] {
+ const themeFontFamilies =
+ themeJson?.settings?.typography?.fontFamilies?.theme;
+ const customFontFamilies =
+ themeJson?.settings?.typography?.fontFamilies?.custom;
+
+ let fontFamilies: any[] = [];
+ if ( themeFontFamilies && customFontFamilies ) {
+ fontFamilies = [ ...themeFontFamilies, ...customFontFamilies ];
+ } else if ( themeFontFamilies ) {
+ fontFamilies = themeFontFamilies;
+ } else if ( customFontFamilies ) {
+ fontFamilies = customFontFamilies;
+ }
+ const bodyFontFamilySetting = themeJson?.styles?.typography?.fontFamily;
+ const bodyFontFamily = getFontFamilyFromSetting(
+ fontFamilies,
+ bodyFontFamilySetting
+ );
+
+ const headingFontFamilySetting =
+ themeJson?.styles?.elements?.heading?.typography?.fontFamily;
+
+ let headingFontFamily;
+ if ( ! headingFontFamilySetting ) {
+ headingFontFamily = bodyFontFamily;
+ } else {
+ headingFontFamily = getFontFamilyFromSetting(
+ fontFamilies,
+ themeJson?.styles?.elements?.heading?.typography?.fontFamily
+ );
+ }
+
+ return [ bodyFontFamily, headingFontFamily ];
+}
+
+function findNearest( input: number, numbers: number[] ): number | null {
+ // If the numbers array is empty, return null
+ if ( numbers.length === 0 ) {
+ return null;
+ }
+ // Sort the array based on the absolute difference with the input
+ numbers.sort( ( a, b ) => Math.abs( input - a ) - Math.abs( input - b ) );
+ // Return the first element (which will be the nearest) from the sorted array
+ return numbers[ 0 ];
+}
+
+function extractFontWeights( fontFaces: any[] ): number[] {
+ const result: number[] = [];
+
+ fontFaces.forEach( ( face ) => {
+ const weights = String( face.fontWeight ).split( ' ' );
+
+ if ( weights.length === 2 ) {
+ const start = parseInt( weights[ 0 ] );
+ const end = parseInt( weights[ 1 ] );
+
+ for ( let i = start; i <= end; i += 100 ) {
+ result.push( i );
+ }
+ } else if ( weights.length === 1 ) {
+ result.push( parseInt( weights[ 0 ] ) );
+ }
+ } );
+
+ return result;
+}
+
+/*
+ * Format the font family to use in the CSS font-family property of a CSS rule.
+ *
+ * The input can be a string with the font family name or a string with multiple font family names separated by commas.
+ * It follows the recommendations from the CSS Fonts Module Level 4.
+ * https://www.w3.org/TR/css-fonts-4/#font-family-prop
+ *
+ * @param input - The font family.
+ * @return The formatted font family.
+ */
+export function formatFontFamily( input: string ): string {
+ // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens).
+ const regex = /^(?!generic\([ a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/;
+ const output = input.trim();
+
+ const formatItem = ( item: string ) => {
+ item = item.trim();
+ if ( item.match( regex ) ) {
+ // removes leading and trailing quotes.
+ item = item.replace( /^["']|["']$/g, '' );
+ return `"${ item }"`;
+ }
+ return item;
+ };
+
+ if ( output.includes( ',' ) ) {
+ return output
+ .split( ',' )
+ .map( formatItem )
+ .filter( ( item ) => item !== '' )
+ .join( ', ' );
+ }
+
+ return formatItem( output );
+}
+
+/**
+ * Gets the preview style for a font family.
+ *
+ * @param family The font family object
+ * @return CSS style object for the font family
+ */
+export function getFamilyPreviewStyle( family: any ): React.CSSProperties {
+ const style: React.CSSProperties = {
+ fontFamily: formatFontFamily( family.fontFamily ),
+ };
+
+ if ( ! Array.isArray( family.fontFace ) ) {
+ style.fontWeight = '400';
+ style.fontStyle = 'normal';
+ return style;
+ }
+
+ if ( family.fontFace ) {
+ //get all the font faces with normal style
+ const normalFaces = family.fontFace.filter(
+ ( face: any ) =>
+ face?.fontStyle && face.fontStyle.toLowerCase() === 'normal'
+ );
+ if ( normalFaces.length > 0 ) {
+ style.fontStyle = 'normal';
+ const normalWeights = extractFontWeights( normalFaces );
+ const nearestWeight = findNearest( 400, normalWeights );
+ style.fontWeight = String( nearestWeight ) || '400';
+ } else {
+ style.fontStyle =
+ ( family.fontFace.length && family.fontFace[ 0 ].fontStyle ) ||
+ 'normal';
+ style.fontWeight =
+ ( family.fontFace.length &&
+ String( family.fontFace[ 0 ].fontWeight ) ) ||
+ '400';
+ }
+ }
+
+ return style;
+}
+
+/**
+ * Iterates through the presets array and searches for slugs that start with the specified
+ * slugPrefix followed by a numerical suffix. It identifies the highest numerical suffix found
+ * and returns one greater than the highest found suffix, ensuring that the new index is unique.
+ *
+ * @param presets The array of preset objects, each potentially containing a slug property.
+ * @param slugPrefix The prefix to look for in the preset slugs.
+ *
+ * @return The next available index for a preset with the specified slug prefix, or 1 if no matching slugs are found.
+ */
+/**
+ * Gets the variation class name for a block style variation.
+ *
+ * @param variation The variation name.
+ * @return The variation class name.
+ */
+export function getVariationClassName( variation: string ): string {
+ if ( ! variation ) {
+ return '';
+ }
+ return `is-style-${ variation }`;
+}
+
+export function getNewIndexFromPresets(
+ presets: any[],
+ slugPrefix: string
+): number {
+ const nameRegex = new RegExp( `^${ slugPrefix }([\\d]+)$` );
+ const highestPresetValue = presets.reduce( ( currentHighest, preset ) => {
+ if ( typeof preset?.slug === 'string' ) {
+ const matches = preset?.slug.match( nameRegex );
+ if ( matches ) {
+ const id = parseInt( matches[ 1 ], 10 );
+ if ( id > currentHighest ) {
+ return id;
+ }
+ }
+ }
+ return currentHighest;
+ }, 0 );
+ return highestPresetValue + 1;
+}
diff --git a/packages/edit-site/src/components/global-styles/variations/style.scss b/packages/global-styles-ui/src/variations/style.scss
similarity index 69%
rename from packages/edit-site/src/components/global-styles/variations/style.scss
rename to packages/global-styles-ui/src/variations/style.scss
index fc6a2184b5c6ad..ffcca831c59baa 100644
--- a/packages/edit-site/src/components/global-styles/variations/style.scss
+++ b/packages/global-styles-ui/src/variations/style.scss
@@ -1,12 +1,12 @@
@use "@wordpress/base-styles/colors" as *;
@use "@wordpress/base-styles/variables" as *;
-.edit-site-global-styles-variations_item {
+.global-styles-ui-variations_item {
box-sizing: border-box;
// To round the outline in Windows 10 high contrast mode.
cursor: pointer;
- .edit-site-global-styles-variations_item-preview {
+ .global-styles-ui-variations_item-preview {
border-radius: $radius-small;
outline: $border-width solid rgba($black, 0.1);
outline-offset: -$border-width;
@@ -26,18 +26,18 @@
}
}
- &:not(.is-active):hover .edit-site-global-styles-variations_item-preview {
+ &:not(.is-active):hover .global-styles-ui-variations_item-preview {
outline-color: rgba($black, 0.3);
}
- &.is-active .edit-site-global-styles-variations_item-preview,
- &:focus-visible .edit-site-global-styles-variations_item-preview {
+ &.is-active .global-styles-ui-variations_item-preview,
+ &:focus-visible .global-styles-ui-variations_item-preview {
outline-color: $gray-900;
outline-offset: $border-width;
outline-width: var(--wp-admin-border-width-focus);
}
- &:focus-visible .edit-site-global-styles-variations_item-preview {
+ &:focus-visible .global-styles-ui-variations_item-preview {
outline-color: var(--wp-admin-theme-color);
}
}
diff --git a/packages/edit-site/src/components/global-styles/variations/variation.js b/packages/global-styles-ui/src/variations/variation.tsx
similarity index 65%
rename from packages/edit-site/src/components/global-styles/variations/variation.js
rename to packages/global-styles-ui/src/variations/variation.tsx
index aa936314652885..2f8ebb39e635e6 100644
--- a/packages/edit-site/src/components/global-styles/variations/variation.js
+++ b/packages/global-styles-ui/src/variations/variation.tsx
@@ -10,32 +10,37 @@ import { Tooltip } from '@wordpress/components';
import { useMemo, useContext, useState } from '@wordpress/element';
import { ENTER } from '@wordpress/keycodes';
import { _x, sprintf } from '@wordpress/i18n';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
-import { privateApis as editorPrivateApis } from '@wordpress/editor';
/**
* Internal dependencies
*/
-import { filterObjectByProperties } from '../../../hooks/use-theme-style-variations/use-theme-style-variations-by-property';
-import { unlock } from '../../../lock-unlock';
+import { GlobalStylesContext } from '../context';
+import { areGlobalStyleConfigsEqual, filterObjectByProperties } from '../utils';
-const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis );
-const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock(
- blockEditorPrivateApis
-);
+interface VariationProps {
+ variation: any;
+ children: ( isFocused: boolean ) => React.ReactNode;
+ isPill?: boolean;
+ properties?: string[];
+ showTooltip?: boolean;
+}
export default function Variation( {
variation,
children,
- isPill,
+ isPill = false,
properties,
- showTooltip,
-} ) {
+ showTooltip = false,
+}: VariationProps ) {
const [ isFocused, setIsFocused ] = useState( false );
- const { base, user, setUserConfig } = useContext( GlobalStylesContext );
+ const {
+ base,
+ user,
+ onChange: setUserConfig,
+ } = useContext( GlobalStylesContext );
const context = useMemo( () => {
- let merged = mergeBaseAndUserConfigs( base, variation );
+ let merged = { ...base, ...variation };
if ( properties ) {
merged = filterObjectByProperties( merged, properties );
}
@@ -43,13 +48,13 @@ export default function Variation( {
user: variation,
base,
merged,
- setUserConfig: () => {},
+ onChange: () => {},
};
}, [ variation, base, properties ] );
const selectVariation = () => setUserConfig( variation );
- const selectOnEnter = ( event ) => {
+ const selectOnEnter = ( event: React.KeyboardEvent ) => {
if ( event.keyCode === ENTER ) {
event.preventDefault();
selectVariation();
@@ -73,23 +78,22 @@ export default function Variation( {
const content = (
setIsFocused( true ) }
onBlur={ () => setIsFocused( false ) }
>
{ children( isFocused ) }
diff --git a/packages/edit-site/src/components/global-styles/variations/variations-color.js b/packages/global-styles-ui/src/variations/variations-color.tsx
similarity index 72%
rename from packages/edit-site/src/components/global-styles/variations/variations-color.js
rename to packages/global-styles-ui/src/variations/variations-color.tsx
index 5d6852c990f3f2..6b3aa49a2934ad 100644
--- a/packages/edit-site/src/components/global-styles/variations/variations-color.js
+++ b/packages/global-styles-ui/src/variations/variations-color.tsx
@@ -10,12 +10,21 @@ import {
* Internal dependencies
*/
import StylesPreviewColors from '../preview-colors';
-import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../../../hooks/use-theme-style-variations/use-theme-style-variations-by-property';
-import Subtitle from '../subtitle';
+import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../hooks';
+import { Subtitle } from '../subtitle';
import Variation from './variation';
-export default function ColorVariations( { title, gap = 2 } ) {
- const propertiesToFilter = [ 'color' ];
+interface ColorVariationsProps {
+ title?: string;
+ gap?: number;
+}
+
+const propertiesToFilter = [ 'color' ];
+
+export default function ColorVariations( {
+ title,
+ gap = 2,
+}: ColorVariationsProps ) {
const colorVariations =
useCurrentMergeThemeStyleVariationsWithUserConfig( propertiesToFilter );
@@ -27,8 +36,8 @@ export default function ColorVariations( { title, gap = 2 } ) {
return (
{ title && { title } }
-
- { colorVariations.map( ( variation, index ) => (
+
+ { colorVariations.map( ( variation: any, index: number ) => (
- style.source === 'block' || variations.includes( style.name )
+function getFilteredBlockStyles(
+ blockStyles: BlockStyle[],
+ variations: string[]
+): BlockStyle[] {
+ return (
+ blockStyles?.filter(
+ ( style ) =>
+ style.source === 'block' || variations.includes( style.name )
+ ) || []
);
}
-export function useBlockVariations( name ) {
+export function useBlockVariations( name: string ): BlockStyle[] {
const blockStyles = useSelect(
( select ) => {
const { getBlockStyles } = select( blocksStore );
@@ -31,13 +45,13 @@ export function useBlockVariations( name ) {
},
[ name ]
);
- const [ variations ] = useGlobalStyle( 'variations', name );
+ const [ variations ] = useSetting( 'variations', name );
const variationNames = Object.keys( variations ?? {} );
return getFilteredBlockStyles( blockStyles, variationNames );
}
-export function VariationsPanel( { name } ) {
+export function VariationsPanel( { name }: VariationsPanelProps ) {
const coreBlockStyles = useBlockVariations( name );
return (
diff --git a/packages/edit-site/src/components/global-styles/variations/variations-typography.js b/packages/global-styles-ui/src/variations/variations-typography.tsx
similarity index 50%
rename from packages/edit-site/src/components/global-styles/variations/variations-typography.js
rename to packages/global-styles-ui/src/variations/variations-typography.tsx
index 65e84a9d965ffe..ba50064a7ec1f8 100644
--- a/packages/edit-site/src/components/global-styles/variations/variations-typography.js
+++ b/packages/global-styles-ui/src/variations/variations-typography.tsx
@@ -10,12 +10,21 @@ import {
* Internal dependencies
*/
import StylesPreviewTypography from '../preview-typography';
-import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../../../hooks/use-theme-style-variations/use-theme-style-variations-by-property';
-import Subtitle from '../subtitle';
+import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../hooks';
+import { Subtitle } from '../subtitle';
import Variation from './variation';
-export default function TypographyVariations( { title, gap = 2 } ) {
- const propertiesToFilter = [ 'typography' ];
+interface TypographyVariationsProps {
+ title?: string;
+ gap?: number;
+}
+
+const propertiesToFilter = [ 'typography' ];
+
+export default function TypographyVariations( {
+ title,
+ gap = 2,
+}: TypographyVariationsProps ) {
const typographyVariations =
useCurrentMergeThemeStyleVariationsWithUserConfig( propertiesToFilter );
@@ -30,24 +39,26 @@ export default function TypographyVariations( { title, gap = 2 } ) {
- { typographyVariations.map( ( variation, index ) => {
- return (
-
- { () => (
-
- ) }
-
- );
- } ) }
+ { typographyVariations.map(
+ ( variation: any, index: number ) => {
+ return (
+
+ { () => (
+
+ ) }
+
+ );
+ }
+ ) }
);
diff --git a/packages/global-styles-ui/src/with-global-styles-provider.tsx b/packages/global-styles-ui/src/with-global-styles-provider.tsx
new file mode 100644
index 00000000000000..83a0e1c29be61c
--- /dev/null
+++ b/packages/global-styles-ui/src/with-global-styles-provider.tsx
@@ -0,0 +1,44 @@
+/**
+ * WordPress dependencies
+ */
+import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
+
+/**
+ * Internal dependencies
+ */
+import { GlobalStylesProvider } from './provider';
+
+interface GlobalStylesProviderProps {
+ value: GlobalStylesConfig;
+ baseValue: GlobalStylesConfig;
+ onChange: ( config: GlobalStylesConfig ) => void;
+}
+
+/**
+ * Higher-order component that wraps a component with GlobalStylesProvider.
+ * This allows components to access GlobalStylesContext without exposing
+ * the provider directly in the public API.
+ *
+ * @param Component - The component to wrap
+ * @return A wrapped component that accepts value, baseValue, and onChange props
+ */
+export function withGlobalStylesProvider< P extends object >(
+ Component: React.ComponentType< P >
+) {
+ return function WrappedComponent( {
+ value,
+ baseValue,
+ onChange,
+ ...props
+ }: P & GlobalStylesProviderProps ) {
+ return (
+
+
+
+ );
+ };
+}
diff --git a/packages/global-styles-ui/tsconfig.json b/packages/global-styles-ui/tsconfig.json
new file mode 100644
index 00000000000000..40ef710a2ce0d0
--- /dev/null
+++ b/packages/global-styles-ui/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "declarationDir": "build-types",
+ "checkJs": false
+ },
+ "references": [
+ { "path": "../a11y" },
+ { "path": "../api-fetch" },
+ { "path": "../block-editor" },
+ { "path": "../components" },
+ { "path": "../compose" },
+ { "path": "../core-data" },
+ { "path": "../date" },
+ { "path": "../data" },
+ { "path": "../element" },
+ { "path": "../global-styles-engine" },
+ { "path": "../i18n" },
+ { "path": "../icons" },
+ { "path": "../keycodes" },
+ { "path": "../private-apis" }
+ ],
+ "include": [ "src/**/*" ],
+ "exclude": [ "src/font-library-modal/lib/**/*" ]
+}
diff --git a/packages/interactivity-router/tsconfig.json b/packages/interactivity-router/tsconfig.json
index 5785e94a169c9f..1a49e679f5e021 100644
--- a/packages/interactivity-router/tsconfig.json
+++ b/packages/interactivity-router/tsconfig.json
@@ -11,4 +11,3 @@
{ "path": "tsconfig.full-page.json" }
]
}
-
\ No newline at end of file
diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts
index b716c2ef5c577e..d21c99a4b13d1f 100644
--- a/packages/private-apis/src/implementation.ts
+++ b/packages/private-apis/src/implementation.ts
@@ -34,6 +34,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [
'@wordpress/fields',
'@wordpress/media-utils',
'@wordpress/upload-media',
+ '@wordpress/global-styles-ui',
];
/**
diff --git a/storybook/main.js b/storybook/main.js
index b1830cc08024e6..0d3bb30491d78a 100644
--- a/storybook/main.js
+++ b/storybook/main.js
@@ -35,6 +35,7 @@ const stories = [
'../packages/components/src/**/stories/*.mdx',
'../packages/icons/src/**/stories/*.story.@(js|tsx|mdx)',
'../packages/edit-site/src/**/stories/*.story.@(js|tsx|mdx)',
+ '../packages/global-styles-ui/src/**/stories/*.story.@(js|tsx|mdx)',
'../packages/dataviews/src/**/stories/*.story.@(js|tsx|mdx)',
'../packages/fields/src/**/stories/*.story.@(js|tsx|mdx)',
].filter( Boolean );
diff --git a/test/e2e/specs/site-editor/style-variations.spec.js b/test/e2e/specs/site-editor/style-variations.spec.js
index 53de08717226a4..fa16194004ef48 100644
--- a/test/e2e/specs/site-editor/style-variations.spec.js
+++ b/test/e2e/specs/site-editor/style-variations.spec.js
@@ -45,9 +45,7 @@ test.describe( 'Global styles variations', () => {
// TODO: instead of locating these elements by class,
// we could update the source code to group them in a
or other container,
// then add `aria-labelledby` and `aria-describedby` etc to provide accessible information,
- const variations = page.locator(
- '.edit-site-global-styles-variations_item'
- );
+ const variations = page.locator( '.global-styles-ui-variations_item' );
await expect( variations ).toHaveCount( 3 );
diff --git a/tsconfig.json b/tsconfig.json
index cf5608b93c69d8..ffdf31a6386672 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -29,6 +29,7 @@
{ "path": "packages/eslint-plugin" },
{ "path": "packages/fields" },
{ "path": "packages/global-styles-engine" },
+ { "path": "packages/global-styles-ui" },
{ "path": "packages/hooks" },
{ "path": "packages/html-entities" },
{ "path": "packages/i18n" },