select( editSiteStore ).getEditorMode(),
+ []
+ );
+ useEffect( () => {
+ if ( editorMode !== 'visual' ) {
+ setIsStyleBookOpened( false );
+ }
+ }, [ editorMode ] );
return (
{ __( 'Styles' ) }
+
+
}
>
-
+ setIsStyleBookOpened( false ) }
+ />
);
}
diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js
new file mode 100644
index 00000000000000..b44b2b7b259b4e
--- /dev/null
+++ b/packages/edit-site/src/components/style-book/index.js
@@ -0,0 +1,193 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ Button,
+ TabPanel,
+ createSlotFill,
+ __experimentalUseSlotFills as useSlotFills,
+} from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+import {
+ getCategories,
+ getBlockTypes,
+ getBlockFromExample,
+ createBlock,
+} from '@wordpress/blocks';
+import { BlockPreview } from '@wordpress/block-editor';
+import { closeSmall } from '@wordpress/icons';
+import { useResizeObserver } from '@wordpress/compose';
+import { useMemo, memo } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { useStyle } from '../global-styles';
+
+const SLOT_FILL_NAME = 'EditSiteStyleBook';
+const { Slot: StyleBookSlot, Fill: StyleBookFill } =
+ createSlotFill( SLOT_FILL_NAME );
+
+function getExamples() {
+ // Use our own example for the Heading block so that we can show multiple
+ // heading levels.
+ const headingsExample = {
+ name: 'core/heading',
+ title: __( 'Headings' ),
+ category: 'text',
+ blocks: [
+ createBlock( 'core/heading', {
+ content: __( 'Code Is Poetry' ),
+ level: 1,
+ } ),
+ createBlock( 'core/heading', {
+ content: __( 'Code Is Poetry' ),
+ level: 2,
+ } ),
+ createBlock( 'core/heading', {
+ content: __( 'Code Is Poetry' ),
+ level: 3,
+ } ),
+ createBlock( 'core/heading', {
+ content: __( 'Code Is Poetry' ),
+ level: 4,
+ } ),
+ createBlock( 'core/heading', {
+ content: __( 'Code Is Poetry' ),
+ level: 5,
+ } ),
+ ],
+ };
+
+ const otherExamples = getBlockTypes()
+ .filter(
+ ( blockType ) =>
+ blockType.name !== 'core/heading' && !! blockType.example
+ )
+ .map( ( blockType ) => ( {
+ name: blockType.name,
+ title: blockType.title,
+ category: blockType.category,
+ blocks: getBlockFromExample( blockType.name, blockType.example ),
+ } ) );
+
+ return [ headingsExample, ...otherExamples ];
+}
+
+function StyleBook( { isSelected, onSelect, onClose } ) {
+ const [ resizeObserver, sizes ] = useResizeObserver();
+ const [ textColor ] = useStyle( 'color.text' );
+ const [ backgroundColor ] = useStyle( 'color.background' );
+ const examples = useMemo( getExamples, [] );
+ const tabs = useMemo(
+ () =>
+ getCategories()
+ .filter( ( category ) =>
+ examples.some(
+ ( example ) => example.category === category.slug
+ )
+ )
+ .map( ( category ) => ( {
+ name: category.slug,
+ title: category.title,
+ icon: category.icon,
+ } ) ),
+ [ examples ]
+ );
+ return (
+
+ 600,
+ } ) }
+ style={ {
+ color: textColor,
+ background: backgroundColor,
+ } }
+ aria-label={ __( 'Style Book' ) }
+ >
+ { resizeObserver }
+
+
+ { ( tab ) => (
+
+ ) }
+
+
+
+ );
+}
+
+const Examples = memo( ( { examples, category, isSelected, onSelect } ) => (
+
+ { examples
+ .filter( ( example ) => example.category === category )
+ .map( ( example ) => (
+ {
+ onSelect( example.name );
+ } }
+ />
+ ) ) }
+
+) );
+
+const Example = memo( ( { title, blocks, isSelected, onClick } ) => (
+
+) );
+
+function useHasStyleBook() {
+ const fills = useSlotFills( SLOT_FILL_NAME );
+ return !! fills?.length;
+}
+
+StyleBook.Slot = StyleBookSlot;
+export default StyleBook;
+export { useHasStyleBook };
diff --git a/packages/edit-site/src/components/style-book/style.scss b/packages/edit-site/src/components/style-book/style.scss
new file mode 100644
index 00000000000000..abcff368dd4bd6
--- /dev/null
+++ b/packages/edit-site/src/components/style-book/style.scss
@@ -0,0 +1,78 @@
+.edit-site-style-book {
+ background: $white; // Fallback color, overriden by JavaScript.
+ border-radius: $radius-block-ui;
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: all 0.3s; // Match .block-editor-iframe__body transition.
+}
+
+.edit-site-style-book__close-button {
+ position: absolute;
+ right: $grid-unit-10;
+ top: math.div($grid-unit-60 - $button-size, 2); // ( tab height - button size ) / 2
+}
+
+.edit-site-style-book__tab-panel {
+ .components-tab-panel__tabs {
+ background: $white;
+ color: $gray-900;
+ }
+
+ .components-tab-panel__tab-content {
+ bottom: 0;
+ left: 0;
+ overflow: auto;
+ padding: $grid-unit-40;
+ position: absolute;
+ right: 0;
+ top: $grid-unit-60; // Height of tabs.
+ }
+}
+
+.edit-site-style-book__examples {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.edit-site-style-book__example {
+ background: none;
+ border-radius: $radius-block-ui;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: $grid-unit-50;
+ margin-bottom: $grid-unit-50;
+ padding: $grid-unit-20;
+ width: 100%;
+
+ &.is-selected {
+ box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
+ }
+
+ .edit-site-style-book.is-wide & {
+ flex-direction: row;
+ }
+}
+
+.edit-site-style-book__example-title {
+ font-size: $default-font-size;
+ font-weight: 500;
+ margin: 0;
+ text-align: left;
+ text-transform: uppercase;
+
+ .edit-site-style-book.is-wide & {
+ text-align: right;
+ width: 120px;
+ }
+}
+
+.edit-site-style-book__example-preview {
+ width: 100%;
+}
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index efc6187a39a6b0..9b871cff4f2d50 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -23,6 +23,7 @@
@import "./components/sidebar-navigation-root/style.scss";
@import "./components/sidebar-navigation-title/style.scss";
@import "./components/site-icon/style.scss";
+@import "./components/style-book/style.scss";
html #wpadminbar {
display: none;
diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js
index 80bdc23994c4b8..92bc48c185cc99 100644
--- a/packages/icons/src/index.js
+++ b/packages/icons/src/index.js
@@ -199,6 +199,7 @@ export { default as rotateLeft } from './library/rotate-left';
export { default as rotateRight } from './library/rotate-right';
export { default as rss } from './library/rss';
export { default as search } from './library/search';
+export { default as seen } from './library/seen';
export { default as separator } from './library/separator';
export { default as settings } from './library/settings';
export { default as shadow } from './library/shadow';
diff --git a/packages/icons/src/library/seen.js b/packages/icons/src/library/seen.js
new file mode 100644
index 00000000000000..4bb328271f834e
--- /dev/null
+++ b/packages/icons/src/library/seen.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const seen = (
+
+);
+
+export default seen;
diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js
new file mode 100644
index 00000000000000..779e54322a9102
--- /dev/null
+++ b/test/e2e/specs/site-editor/style-book.spec.js
@@ -0,0 +1,131 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.use( {
+ styleBook: async ( { page }, use ) => {
+ await use( new StyleBook( { page } ) );
+ },
+} );
+
+test.describe( 'Style Book', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'emptytheme' );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ } );
+
+ test.beforeEach( async ( { admin, siteEditor, styleBook, page } ) => {
+ await admin.visitSiteEditor();
+ await siteEditor.enterEditMode();
+ await styleBook.open();
+ await expect(
+ page.locator( 'role=region[name="Style Book"i]' )
+ ).toBeVisible();
+ } );
+
+ test( 'should disable toolbar butons when open', async ( { page } ) => {
+ await expect(
+ page.locator( 'role=button[name="Toggle block inserter"i]' )
+ ).not.toBeVisible();
+ await expect(
+ page.locator( 'role=button[name="Tools"i]' )
+ ).not.toBeVisible();
+ await expect(
+ page.locator( 'role=button[name="Undo"i]' )
+ ).not.toBeVisible();
+ await expect(
+ page.locator( 'role=button[name="Redo"i]' )
+ ).not.toBeVisible();
+ await expect(
+ page.locator( 'role=button[name="Show template details"i]' )
+ ).not.toBeVisible();
+ await expect(
+ page.locator( 'role=button[name="View"i]' )
+ ).not.toBeVisible();
+ } );
+
+ test( 'should have tabs containing block examples', async ( { page } ) => {
+ await expect( page.locator( 'role=tab[name="Text"i]' ) ).toBeVisible();
+ await expect( page.locator( 'role=tab[name="Media"i]' ) ).toBeVisible();
+ await expect(
+ page.locator( 'role=tab[name="Design"i]' )
+ ).toBeVisible();
+ await expect(
+ page.locator( 'role=tab[name="Widgets"i]' )
+ ).toBeVisible();
+ await expect( page.locator( 'role=tab[name="Theme"i]' ) ).toBeVisible();
+
+ await expect(
+ page.locator(
+ 'role=button[name="Open Headings styles in Styles panel"i]'
+ )
+ ).toBeVisible();
+ await expect(
+ page.locator(
+ 'role=button[name="Open Paragraph styles in Styles panel"i]'
+ )
+ ).toBeVisible();
+
+ await page.click( 'role=tab[name="Media"i]' );
+
+ await expect(
+ page.locator(
+ 'role=button[name="Open Image styles in Styles panel"i]'
+ )
+ ).toBeVisible();
+ await expect(
+ page.locator(
+ 'role=button[name="Open Gallery styles in Styles panel"i]'
+ )
+ ).toBeVisible();
+ } );
+
+ test( 'should open correct Global Styles panel when example is clicked', async ( {
+ page,
+ } ) => {
+ await page.click(
+ 'role=button[name="Open Headings styles in Styles panel"i]'
+ );
+
+ await expect(
+ page.locator(
+ 'role=region[name="Editor settings"i] >> role=heading[name="Heading"i]'
+ )
+ ).toBeVisible();
+ } );
+
+ test( 'should disappear when closed', async ( { page } ) => {
+ await page.click(
+ 'role=region[name="Style Book"i] >> role=button[name="Close Style Book"i]'
+ );
+
+ await expect(
+ page.locator( 'role=region[name="Style Book"i]' )
+ ).not.toBeVisible();
+ } );
+} );
+
+class StyleBook {
+ constructor( { page } ) {
+ this.page = page;
+ }
+
+ async disableWelcomeGuide() {
+ // Turn off the welcome guide.
+ await this.page.evaluate( () => {
+ window.wp.data
+ .dispatch( 'core/preferences' )
+ .set( 'core/edit-site', 'welcomeGuideStyles', false );
+ } );
+ }
+
+ async open() {
+ await this.disableWelcomeGuide();
+ await this.page.click( 'role=button[name="Styles"i]' );
+ await this.page.click( 'role=button[name="Open Style Book"i]' );
+ }
+}